Skip to main content

Sign in to CleanKata

Track your progress, earn XP, and unlock every lesson.

By signing in you agree to our Terms of Use and Privacy Policy.

Clean Architecture60 XP6 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.

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.

🔧 Some exercises may still have errors. If something seems wrong, use the Feedback button (bottom-right of the page) to report it — it helps us fix it fast.

Hint: Package by Layer is the right starting point for tutorials, but the wrong ending point for real systems. Switch when you can't find things anymore.

✗ Your version

Organizational Strategies: Package by Layer — CleanKata — CleanKata