Skip to main content
Clean Architecture 60 XP · 6 min

Organizational Strategies: Package by Layer

The simplest code organization groups by technical layer (web/service/repository) — easy to start but hides business intent and allows accidental cross-layer shortcuts.

Showing
Ad (728×90)

Why this matters

Package by Layer is the most common starting structure: controllers/, services/, repositories/. Every framework tutorial uses it. It is universally understood and takes minutes to set up. For a three-feature prototype, it is perfectly fine.

The problem surfaces at scale. A system with 50 features has 150 files in 3 directories. Opening the project in an IDE tells you nothing about what the system does — you see layer names, not domain names. Adding "Orders" requires touching three separate directories. A developer adding a feature must mentally maintain the connection between three scattered files.

Access enforcement is absent. Because everything in services/ is typically public, a controller can import a repository directly and bypass the service layer entirely. No compiler stops this. The result is a slow drift toward a "big ball of mud" — a structure that looks layered but behaves like spaghetti.

Package by Layer is not wrong. It is the right starting point for tutorials and small projects, and the wrong ending point for anything that grows. Recognizing when to migrate is a key architectural skill.

The problem

50 features × 3 layers = 150 files in 3 giant directories. No domain visibility. Adding a feature requires touching 3 separate packages with zero cohesion.

Bad

project/
├── controllers/
│   ├── order_controller.py    # what does this system DO? Can't tell.
│   ├── user_controller.py
│   ├── payment_controller.py
│   └── ... (47 more files)
├── services/
│   ├── order_service.py
│   ├── user_service.py
│   ├── payment_service.py
│   └── ... (47 more files)
└── repositories/
    ├── order_repo.py
    ├── user_repo.py
    ├── payment_repo.py
    └── ... (47 more files)

# Adding "Shipping" feature:
#   1. controllers/shipping_controller.py
#   2. services/shipping_service.py
#   3. repositories/shipping_repo.py
# → Touch 3 packages. Zero cohesion. Domain invisible.
src/
├── controllers/
│   ├── OrderController.ts     // what does this system DO? Can't tell.
│   ├── UserController.ts
│   ├── PaymentController.ts
│   └── ... (47 more files)
├── services/
│   ├── OrderService.ts
│   ├── UserService.ts
│   ├── PaymentService.ts
│   └── ... (47 more files)
└── repositories/
    ├── OrderRepository.ts
    ├── UserRepository.ts
    ├── PaymentRepository.ts
    └── ... (47 more files)

// Adding "Shipping" feature:
//   1. controllers/ShippingController.ts
//   2. services/ShippingService.ts
//   3. repositories/ShippingRepository.ts
// → Touch 3 packages. Zero cohesion. Domain invisible.

The solution

Same code, same classes — organized by feature instead of by layer. Domain is visible at the top level. Each feature is cohesive. Adding "Shipping" means one new directory.

✓ Good — Package by Feature

project/
├── orders/
│   ├── order_controller.py    # all Order code in one place
│   ├── order_service.py
│   └── order_repo.py
├── users/
│   ├── user_controller.py
│   ├── user_service.py
│   └── user_repo.py
├── payments/
│   ├── payment_controller.py
│   ├── payment_service.py
│   └── payment_repo.py
└── inventory/
    ├── inventory_controller.py
    ├── inventory_service.py
    └── inventory_repo.py

# Adding "Shipping" feature:
#   1. Create shipping/ directory
#   2. Add three cohesive files inside it
# → One package. Cohesion high. Domain visible. What does this do? Obvious.
src/
├── orders/
│   ├── OrderController.ts     // all Order code in one place
│   ├── OrderService.ts
│   └── OrderRepository.ts
├── users/
│   ├── UserController.ts
│   ├── UserService.ts
│   └── UserRepository.ts
├── payments/
│   ├── PaymentController.ts
│   ├── PaymentService.ts
│   └── PaymentRepository.ts
└── inventory/
    ├── InventoryController.ts
    ├── InventoryService.ts
    └── InventoryRepository.ts

// Adding "Shipping" feature:
//   1. Create src/shipping/ directory
//   2. Add three cohesive files inside it
// → One package. Cohesion high. Domain visible. What does this do? Obvious.

Key takeaway

Package by Layer is the right starting point for tutorials, but the wrong ending point for real systems. Switch to Package by Feature when you can't find things anymore — when adding a feature requires hunting across three directories, the structure is working against you, not for you.

Done with this lesson?

Mark it complete to earn XP and track your progress.