Why this matters
Package by Feature organizes code so that the directory structure reveals what the system does. Instead of controllers/, services/, repositories/, the top level reads orders/, users/, payments/. This is what Robert Martin calls Screaming Architecture — the structure announces the business domain, not the technology stack.
Advantages: All code related to Orders lives together. Finding "how does the order discount work?" means opening one directory instead of three. Deleting a feature means deleting one directory. Onboarding a new developer: "where does checkout live?" — in checkout/.
The limitation: Package by Feature improves discoverability but does not enforce boundaries. Inside orders/, a controller can still directly import a repository from users/. Feature packages can become tangled with each other through hidden imports. The structure is better, but the discipline must be added separately — through access modifiers, barrel files, or explicit ports.
The problem
Feature packages exist but boundaries are violated. orders/ reaches into users/ internals, bypassing the public API and creating hidden cross-feature coupling.
Bad
# orders/order_controller.py
from orders.order_service import OrderService
from users.user_repository import UserRepository # ← crosses feature boundary!
class OrderController:
def place_order(self):
data = request.json
user_id = data["user_id"]
# Reaches INTO the users/ package internals — bypasses UserService
user_repo = UserRepository()
user = user_repo.find_by_id(user_id)
shipping_address = user.shipping_address # raw entity from another feature
return jsonify(OrderService().place(data, shipping_address))
# UserRepository is now coupled to OrderController.
# Refactoring UserRepository breaks OrderController — invisible dependency.
// orders/OrderController.ts
import { UserRepository } from "../users/UserRepository"; // ← crosses boundary!
import { OrderService } from "./OrderService";
export class OrderController {
async placeOrder(req: { body: { userId: string } }): Promise
The solution
Feature packages expose only a public service API. Internals (like UserRepository) are never exported outside the package. Cross-feature communication uses the public API only.
Good
# users/user_service.py ← public API of the users/ feature
from dataclasses import dataclass
from users._user_repository import UserRepository # private, underscore prefix
@dataclass
class ShippingAddress:
street: str
city: str
country: str
class UserService:
def get_shipping_address(self, user_id: str) -> ShippingAddress:
repo = UserRepository()
user = repo.find_by_id(user_id)
return ShippingAddress(street=user.street, city=user.city, country=user.country)
# orders/order_controller.py ← uses only the public API
from users.user_service import UserService # public API only
from orders.order_service import OrderService
class OrderController:
def place_order(self):
data = request.json
address = UserService().get_shipping_address(data["user_id"])
return jsonify(OrderService().place(data, address))
# UserRepository can be refactored freely — OrderController never touches it.
// users/UserService.ts ← public API of the users/ feature
export interface ShippingAddress { street: string; city: string; country: string; }
export class UserService {
async getShippingAddress(userId: string): Promise {
const repo = new UserRepository(); // internal
const user = await repo.findById(userId);
return { street: user.street, city: user.city, country: user.country };
}
}
// users/index.ts ← barrel: only exports public API
export { UserService } from "./UserService";
export type { ShippingAddress } from "./UserService";
// UserRepository is NOT exported
// orders/OrderController.ts ← uses only the barrel (public API)
import { UserService } from "../users"; // barrel import only
import { OrderService } from "./OrderService";
export class OrderController {
async placeOrder(req: { body: { userId: string } }): Promise
Key takeaway
Package by Feature improves discoverability but doesn't enforce boundaries on its own. You need access modifiers or explicit barrel files to prevent features from reaching into each other's internals. The structure tells you where things are; access control tells you what can call what.