About this article
This article is the third deep dive in the “Software Architecture” category of the Architecture Crash Course for the Generative-AI Era series, covering module design inside the app.
How you draw the rooms inside the app directly determines the code’s lifespan. The article compares four patterns (Layered / Hexagonal / Onion / Clean), shows selection across three axes (domain complexity, team skill, project lifetime), and lands a practical guideline: “don’t watch the pattern name — watch the dependency arrows.”
What is module design in the first place
Module design is, in a nutshell, “deciding the floor plan of which room each piece of code goes in inside the app.”
Imagine an office floor plan. If you mix the sales, engineering, and accounting departments on one open floor, phone calls and conversations interfere and everyone’s productivity drops. Partition them into rooms and route interactions through reception desks (interfaces), and each department can work independently and efficiently. Software module design works the same way: clearly defining “room assignments” like the UI layer, business-logic layer, and data layer creates a structure where changes in one room don’t ripple into others.
Why module design matters
What happens if module design is left vague? Bad module splits break any overall structure. Monoliths become spaghetti, modular monoliths become boundary-in-name-only, microservices’ internals turn chaotic.
Trying to fix it later means a large rebuild in practice. A codebase where every feature change triggers “can I write it here?” debates becomes unmaintainable in years; new-feature dev speed halves. Deciding it deliberately at the start is cheapest long-term.
Internal “room layout” decides lifespan
Bad module splits break any overall structure. Monoliths become spaghetti, modular monoliths become boundary-in-name-only, microservices’ internals turn chaotic. Module design is the basic conditioning underlying overall structure.
Trying to fix it later means a large rebuild in practice. A codebase where every feature change triggers “can I write it here?” debates becomes unmaintainable in years; new-feature dev speed halves. Deciding it deliberately at the start is cheapest long-term.
Why patterns matter
Multi-person teams can’t argue about where to write code every time and stay productive. Years of experience produced “follow this pattern and you won’t break” — that’s an architecture pattern.
Following a pattern lets new engineers immediately see “this is the UI layer,” “this is the business-logic layer,” and predict the impact of changes. But if adopting a pattern becomes the goal, you fall into “over-engineering kills productivity.”
| Goal | Effect |
|---|---|
| Standardize where code lives | Newcomers don’t get lost |
| Organize dependencies | Predictable change impact |
| Make testing easier | Early bug detection |
| Survive future changes | Tech-stack swaps possible |
The four major patterns
Four widely used in practice. All share “controlling the direction of dependencies”:
| Pattern | Quick trait |
|---|---|
| Layered Architecture | Horizontal split: UI / business / data. The classic |
| Hexagonal Architecture | Domain at the center, ports / adapters isolating external connections |
| Onion Architecture | Concentric layers, domain at the innermost |
| Clean Architecture | Onion’s evolution. One-way inward dependency rule |
Hexagonal, Onion, and Clean are “essentially the same in spirit.” Differences in detail; the substance is “domain-centric, dependency-direction control.” Sectarian arguments are a waste of time.
Layered Architecture
Layered Architecture stacks UI / business logic / data access horizontally — the classic pattern. Web’s MVC (Model-View-Controller), desktop’s MVP (Model-View-Presenter) and MVVM (Model-View-ViewModel) are layered variants. Almost all books and framework intros start with this — biggest strength is “every team has shared understanding.”
The weakness: business-logic layer easily depends directly on data-access layer; DB-implementation concerns leak into business logic. OR Mapper (OR Mapping — library that maps DB tables to objects) types and table structure bleed across the app, exploding migration effort when you want to change DBs — common failure mode.
| Strengths | Weaknesses |
|---|---|
| Low learning cost | Business layer tends to depend on DB implementation |
| Every team is familiar | More layers = more thin pass-throughs |
| Natural fit with framework defaults | Tests tend to require DB |
MVC and similar standard frameworks use this shape. Still primary at small / mid scale.
Hexagonal Architecture
Hexagonal Architecture places the domain (business logic) at the center and inserts ports (interfaces) and adapters (implementations) at external connection points. UI / DB / external API — all the “outside world” — accesses the domain only through adapters.
Biggest benefit: “freely swap external dependencies.” Replace the DB with an in-memory implementation in tests, change the production message queue, swap UI from web to CLI — none of these affect domain logic. Easy to test, durable for long-term operation.
| Strengths | Weaknesses |
|---|---|
| Easy testing | Overspec at small scale |
| Easy to swap external dependencies | Port / adapter design skill required |
| Domain stays independent and protected | More classes |
| Strong long-term operation | Mid-high learning cost |
Combined with DDD (Domain-Driven Design) is where it shines. The favorite for mid-scale and above.
Onion / Clean Architecture
Clean Architecture, proposed by Robert C. Martin, “fixes dependencies in one direction inward.” Four concentric layers: “Frameworks & Drivers,” “Interface Adapters,” “Use Cases,” “Entities.” Rule: “outer layers may depend on inner; inner layers don’t know outer.”
This makes Entities (business rules) the most stable core; UI-framework or DB changes don’t ripple into business logic. The Dependency Inversion Principle (DIP) is the key — inner defines interfaces, outer provides implementations, effectively reversing dependency direction.
flowchart LR
subgraph FW["Frameworks & Drivers (outermost)"]
FW1[DB / Web / UI / external APIs]
end
subgraph IA["Interface Adapters"]
IA1[Controller / Gateway / DTO conversion]
end
subgraph UC["Use Cases"]
UC1[App-specific business flows]
end
subgraph EN["Entities (innermost)"]
EN1[Business rules<br/>most stable]
end
FW -->|deps OK| IA -->|deps OK| UC -->|deps OK| EN
EN -.|reverse forbidden|.-> FW
classDef outer fill:#fae8ff,stroke:#a21caf;
classDef mid1 fill:#fef3c7,stroke:#d97706;
classDef mid2 fill:#f0f9ff,stroke:#0369a1;
classDef core fill:#dbeafe,stroke:#2563eb,stroke-width:3px;
class FW outer;
class IA mid1;
class UC mid2;
class EN core;
| Layer | Role |
|---|---|
| Entities (innermost) | Business rules; most stable |
| Use Cases | Business flows; app-specific logic |
| Interface Adapters | DTO (Data Transfer Object — simple struct for inter-layer data passing) conversion, Controller, Presenter |
| Frameworks & Drivers (outermost) | DB, Web, UI framework |
Onion is close to Clean’s archetype; substance is identical.
Four-pattern comparison
The four patterns are organized by “learning cost vs testability trade-off.” Layered: easy to learn but weak long-term; Clean: strong long-term but high initial cost.
| Aspect | Layered | Hexagonal | Onion | Clean |
|---|---|---|---|---|
| Learning cost | ◎ Low | ◯ Mid | △ Mid-high | △ High |
| Small-scale fit | ◎ | ◯ | △ | × |
| Testability | △ | ◎ | ◎ | ◎ |
| Change resilience | △ | ◎ | ◎ | ◎ |
| Initial cost | Low | Mid | Mid-high | High |
| Suitable team | Beginner+ | Mid+ | Mid-experienced | Experienced |
In general the order Layered → Hexagonal → Clean increases both difficulty and benefits.
Decision criteria
1. Domain complexity
Most important is the complexity of the business domain. For CRUD-centric simple work (internal application management, simple blogs), introducing complex dependency control yields little return — over-engineering.
Conversely, in domains with complex business logic and many rules (insurance, finance, healthcare, e-commerce, logistics), the value of separating business logic from external dependencies jumps. When framework / DB concerns leak into business rules, “a business change becomes an infrastructure change” recurs frequently.
| Domain complexity | Suitable design |
|---|---|
| Simple (CRUD-centric) | Layered |
| Moderate | Hexagonal |
| High (many business rules) | Clean + DDD |
“Complexity” is judged by “rule volume / change frequency,” not feature count.
2. Team skill
Pattern is “a form,” so the team must use it correctly. Introducing Clean Architecture to a team learning it tends to produce “name-only Clean, actually thick Layered” — a sad outcome.
If everyone is familiar with Layered at small/mid scale, forcing Clean isn’t right; “writing clean Layered is far more productive.” Adopt patterns gradually matching team maturity.
- New-grad-centric / early startup → Layered
- Mid-career-centric / product stable phase → Hexagonal
- Experienced lineup / complex domain → Clean + DDD
“The pattern the team can run” beats “the superior pattern” every time.
3. Project lifetime
Project lifetime expectations drive selection too. For a few-year-rebuild-assumed prototype or MVP, complex dependency control isn’t worth it; Layered’s fastest path is enough.
For 10+-year core systems and long-term SaaS products, surviving framework / DB generational shifts matters; Hexagonal or Clean investment pays back later. Whether you can update the framework 5-10 years later directly tied to module design quality.
| Project lifetime | Recommended |
|---|---|
| Up to 3 years (MVP / prototype) | Layered |
| 3-10 years (typical SaaS / business app) | Hexagonal |
| 10+ years (core systems / long-term SaaS) | Clean + DDD |
Selection by case
Startup MVP / prototype
Layered. At fastest-path stage, release speed beats strict dependency control. Plan for rebuilding later; commit to that.
Business app / SaaS for mid-long-term operation
Hexagonal. Best testability vs change-resilience balance; realistic optimum for many projects.
Complex-domain enterprise
Clean + DDD. With many business rules and frequent changes, Clean Architecture’s investment pays back over time.
CRUD-centric simple app
Layered. Complex structure on a thin business-logic domain just adds classes without return.
Project-size × pattern ladder
Note: industry rates as of April 2026. Periodic refresh required.
Module-design pattern is decided not by “which is correct” but by “how many lines, how many people, how many years.” Empirical ladder:
| Codebase | Team size | Operating years | Recommended pattern | File-line target |
|---|---|---|---|---|
| Up to 10k LOC | 1-3 | up to 3 yr | Plain MVC / Layered | <= 300 lines |
| Up to 50k LOC | 3-10 | 3-10 yr | Layered or Hexagonal | <= 300 lines |
| Up to 200k LOC | 10-30 | 5-15 yr | Hexagonal or Clean | <= 300 lines |
| 200k LOC+ | 30+ | 10+ yr | Clean + DDD | <= 300 lines |
”<= 300 lines/file, <= 50 lines/method, max 3 nesting, cyclomatic complexity 10” is the quantitative guardrail many adopt. Crossing those lines is the split signal — modern tools like ESLint and SonarQube auto-detect. “Code that doesn’t fit in one method per screen (~80 lines) has wrong design” is the rule of thumb.
Patterns escalate with scale. Clean for an MVP is excess; staying Layered on a giant project is a recipe for breakdown.
Module-design traps
Even with patterns adopted, stepping on these produces “pattern-name spaghetti.”
| Forbidden move | Why |
|---|---|
| God class / God Module (thousands of lines, dozens of responsibilities) | Canonical SRP violation. 300+ lines is the split signal |
| Cyclic dependencies (A→B→A) | Cycles between packages always breed bugs. Detect with ESLint (import/no-cycle) |
| Use the ORM-generated Entity across all layers | DB-schema changes propagate to UI. Convert via DTOs at every boundary |
| Fat Service (all business logic in Service) | Falls into anemic domain model — Entity becomes mere data structure. Push logic into Entity / Value Object |
| Separate interface (port) one-to-one with implementation | Abstraction without swap candidates is overspec. Abstract only when there are 2+ swap candidates |
| Mock your own code in tests | Mocking repository / use-case layers hides integration bugs. Mock only the external world |
| Don’t keep ADRs (Architecture Decision Record) | 5 years later, “why did we pick this pattern?” is lost; wrong-direction “improvements” enter |
| Borrow only the pattern name without reading the original | Implementation diverges from intent; spaghetti with a pattern name |
| Apply Clean Architecture to a CRUD app | Over-engineering that doesn’t pay for itself against business value. Layered is enough |
| Adopt DDD pattern names only without structuring domain vocabulary | Mimicking Repository and Aggregate shapes without modeling the domain just adds classes for nothing |
Books like Martin Fowler’s 2006 “Patterns of Enterprise Application Architecture” and Robert C. Martin’s 2017 “Clean Architecture” — “copying the name without reading the original” is the worst failure. Patterns are “discipline on the writing side”; teams that can’t enforce the rules don’t benefit.
Cyclic dependencies, God Module, all-layer ORM bleed-through are three immediate-death forbidden moves regardless of pattern.
Common principles
Whatever pattern you pick, always follow these. Not pattern-specific — they’re the foundation of good design generally. Code that borrows pattern names without honoring principles ends up the same as spaghetti.
Code that ignores principles always falls into “touch and break” state. Cycles propagate one-class changes across many; mixing domain and infra turns “want to change DB” into “rewrite business logic”; vague responsibility scatters the same logic across three places.
- Dependencies flow one direction (no cycles).
- Domain logic stays independent of UI / DB.
- Layer / module responsibilities clear.
- Tests structured as unit / integration / E2E.
If principles are honored, pattern names come second. Distorting implementation to fit a pattern name is the worst.
AI decision axes
| AI-era favorable | AI-era unfavorable |
|---|---|
| Small, self-contained modules | God Module (giant, multi-responsibility) |
| Interfaces making dependency direction explicit | Implicit dependencies, scope-crossing |
| Maintained type-definition files (.d.ts, etc.) | Untyped dynamic calls |
| Standard patterns like Layered / Clean | Idiosyncratic structures |
- Narrow candidates by domain complexity (CRUD-centric or rich business rules).
- Pick a pattern matching team maturity (an unrunnable pattern is worthless).
- Justify initial investment by project lifetime.
- Design module boundaries as AI-context units.
”Are we doing this for 7 screens?” (industry case)
Right after reading the Clean Architecture book, a junior engineer creates 5 files (Entity, UseCase, Repository, Controller, Presenter) for a single simple-form screen. The senior calmly asks “are we doing this for 7 screens?”; the engineer comes back to reality and ends up with a more Layered-style simple structure.
Personal stories of falling into the same trap and getting sent back in review with “first try writing 1 file per screen” are common. Pattern adoption is judged by “does it match domain complexity”, not by the heat from the book you just read.
Applying 4-layer structure to CRUD screens lines up many similar DTOs, with effort not matching business value. The sense of “design matched to complexity” may need failure to internalize.
Recording design intent in ADR lets successors trace why this pattern was picked.
What you must decide — what’s your project’s answer?
Articulate your project’s answer in 1-2 sentences for each:
- Which pattern to adopt (Layered / Hexagonal / Clean)
- Where to draw module boundaries (per feature / per domain)
- Dependency-direction rules (inner doesn’t know outer, etc.) — agreed on team
- Test strategy (unit / integration / E2E ratios)
- Whether to record the choice as an ADR
- Future-pattern-change scenario assumed
Summary
This article covered module design — the four patterns of Layered, Hexagonal, Onion, Clean across scale, complexity, lifetime.
CRUD-centric: Layered. Mid-scale long-term: Hexagonal. Complex domain: Clean + DDD. Don’t make pattern names the goal; keep dependency direction one-way and any pattern stands up in practice.
The next article covers API design (REST / GraphQL / gRPC / WebSocket).
Back to series TOC -> ‘Architecture Crash Course for the Generative-AI Era’: How to Read This Book
I hope you’ll read the next article as well.
📚 Series: Architecture Crash Course for the Generative-AI Era (20/89)