Why this matters
Package by Component is Simon Brown's synthesis of Package by Layer and Package by Feature. A component is a coarse-grained unit with a well-defined public interface and hidden internals. Everything required to implement one complete domain boundary — controller, service, repository, entity — lives in one package. The key addition over Package by Feature: only the service is public. Everything else is package-private.
The compiler becomes the enforcer. In Java, package-private access means no other package can access the class even if they know its name. In TypeScript, a barrel index.ts that exports only the service makes the repository invisible to the rest of the system — any import of the internal file is caught by ESLint rules. In Python, underscore-prefixed modules and __all__ provide the same intent.
This is packaging by architectural boundary, not by technical layer or domain feature. Each component owns its full vertical slice and enforces its own surface area.
The problem
OrderRepository is fully public. A PaymentsController imports it directly — bypassing OrderService and all its business logic.
Bad
# orders/order_repository.py ← should be internal, but is fully public
class OrderRepository:
def find_by_id(self, order_id: str) -> dict:
return {"id": order_id, "status": "pending", "total": 99.0}
# orders/order_service.py ← intended public API
class OrderService:
def get_order_status(self, order_id: str) -> str:
return OrderRepository().find_by_id(order_id)["status"]
# payments/payments_controller.py ← violates the component boundary
from orders.order_repository import OrderRepository # ← bypasses OrderService!
class PaymentsController:
def process(self, order_id: str):
order = OrderRepository().find_by_id(order_id) # direct internal access
# ... no business rules applied, no validation ...
# The component has no boundary — it's just a folder with a name.
// orders/OrderRepository.ts ← should be internal, but is exported
export class OrderRepository {
findById(orderId: string): { id: string; status: string; total: number } {
return { id: orderId, status: "pending", total: 99.0 };
}
}
// orders/OrderService.ts ← intended public API
export class OrderService {
getOrderStatus(orderId: string): string {
return new OrderRepository().findById(orderId).status;
}
}
// payments/PaymentsController.ts ← violates the component boundary
import { OrderRepository } from "../orders/OrderRepository"; // ← bypasses service!
export class PaymentsController {
process(orderId: string): void {
const order = new OrderRepository().findById(orderId); // direct internal access
// no business rules applied
}
}
// The component has no boundary — it's just a folder with a name.
The solution
Only OrdersService is exported from the component. OrderRepository is package-private. Payments can only call OrdersService.get_order_status() — the boundary is compiler-enforced.
Good
# orders/__init__.py ← enforced public API
from orders._order_service import OrderService
__all__ = ["OrderService"] # only OrderService is exported
# orders/_order_repository.py ← private (underscore prefix)
class _OrderRepository:
def find_by_id(self, order_id: str) -> dict:
return {"id": order_id, "status": "pending", "total": 99.0}
# orders/_order_service.py ← private implementation
from orders._order_repository import _OrderRepository
class OrderService:
def get_order_status(self, order_id: str) -> str:
return _OrderRepository().find_by_id(order_id)["status"]
def place_order(self, data: dict) -> dict:
order = {"id": data["id"], "total": data["total"], "status": "pending"}
_OrderRepository().save(order)
return order
# payments/payments_controller.py ← only sees the public API
from orders import OrderService # _OrderRepository is not accessible
class PaymentsController:
def process(self, order_id: str):
status = OrderService().get_order_status(order_id) # goes through business logic
// orders/OrderRepository.ts ← NOT in the barrel (internal only)
class OrderRepository { // no export keyword at top level
findById(orderId: string): { id: string; status: string; total: number } {
return { id: orderId, status: "pending", total: 99.0 };
}
}
export { OrderRepository }; // visible inside orders/ only
// orders/OrdersService.ts
import { OrderRepository } from "./OrderRepository";
export class OrdersService {
getOrderStatus(orderId: string): string {
return new OrderRepository().findById(orderId).status;
}
placeOrder(data: { id: string; total: number }): object {
const order = { id: data.id, total: data.total, status: "pending" };
// new OrderRepository().save(order);
return order;
}
}
// orders/index.ts ← barrel: only public surface
export { OrdersService } from "./OrdersService";
// OrderRepository is NOT re-exported
// payments/PaymentsController.ts ← only sees the barrel
import { OrdersService } from "../orders"; // barrel import
export class PaymentsController {
process(orderId: string): void {
const status = new OrdersService().getOrderStatus(orderId); // through business logic
}
}
// ESLint: import/no-internal-modules catches any "../orders/OrderRepository" import.
Key takeaway
If internal classes of a component are public, the component has no boundary — it is just a folder with a name. Use access modifiers, barrel files, and linting rules to enforce that the only entry point to a component is its declared public API. The compiler should make violations impossible, not merely impolite.