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.”
Before you read this
This article is mostly about development: programs and APIs. If IT vocabulary is unfamiliar, reading the primer "Programs and APIs" first makes it far easier to follow. You can also look anything up in the glossary as you read.
What is module design in the first place
Module design is “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.
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.
That is why the established patterns exist at all.
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 main 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 — the first candidate at small to mid scale
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 — the pick from mid scale upwards
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 is where it shines. The favorite for mid-scale and above.
Onion and Clean — a long-term investment for complex domains
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.
| 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.
The four patterns compared
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.
Across all four, a few principles are common.
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.
How to choose — decide it on three axes
Axis 1: how complex the domain is
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 grows significantly. 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.
Axis 2: what the team can already do
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.
Axis 3: how long the project will live
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 |
By case, the choice lands like this.
A startup MVP or prototype. Layered. At fastest-path stage, release speed beats strict dependency control. Plan for rebuilding later; commit to that.
A business application or SaaS meant to run for years. Hexagonal. Best testability vs change-resilience balance; realistic optimum for many projects.
An enterprise system with a complex domain. Clean + DDD. With many business rules and frequent changes, Clean Architecture’s investment pays back over time.
A simple, CRUD-centric application. Layered. Complex structure on a thin business-logic domain just adds classes without return.
A practical 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.
Three scenarios
If you are building solo or at a startup
Aim for the fastest release with plain MVC or a layered structure. Bringing the layer structure of Clean Architecture into an MVP you expect to rebuild within a few years is over-design of the textbook kind. Automatically checking quantitative guardrails — 300 lines per file, say — with ESLint is on its own enough to keep order.
If you are a small or mid-size SaaS
Around the point where the codebase passes fifty thousand lines and the team passes three to ten people, it becomes time to consider a staged move to hexagonal (ports and adapters). Separating the business logic from external dependencies is what buys the resilience to survive switching payment provider or changing database.
If you are a large enterprise
For a core system in a complex domain — insurance, finance, logistics — the investment in Clean Architecture and DDD pays back. The team’s level of understanding is a precondition, though. Introduce it to a team still learning and only the pattern names arrive first, so bring it in together with a training plan.
AI decision axes — A module is the AI’s unit of context
Module size and AI context windows
Current major LLMs have context windows of 100K-1M tokens, but accuracy concentrates on the front portion. Even if you feed a 3,000-line God Module whole, AI cannot understand it uniformly. Conversely, a module that’s under 300 lines with a single responsibility can receive modification instructions as a standalone file.
In other words, module-splitting granularity directly becomes “the work unit AI can accurately handle in one go.” Under-300-line units are also easier for human review, so this direction is good design for humans too.
Without explicit dependency direction, AI runs wild
When delegating code generation to AI, it will write code directly referencing other modules if explicit interfaces are absent. For example, importing DB connection libraries directly from the domain layer, or calling repositories directly from UI components — boundary violations happen.
If dependency directions are made explicit through interfaces (TypeScript’s export type / Protocol, Go’s interface), AI concentrates on writing code that satisfies the given types. Circular dependencies can also be detected by lint, functioning as a quality gate for AI-generated code.
Choosing standard patterns dramatically improves AI generation accuracy
Clean Architecture and Layered Architecture have massive implementation examples in training data. If you instruct AI “write a UserService in Layered style,” code following the Controller→Service→Repository flow comes out with high probability.
On the other hand, custom variant structures (e.g., a company-proprietary mix of CQRS and Hexagonal) have almost no training data, requiring project-specific rules to be attached as context every time. As this overhead accumulates, AI utilization costs boomerang back.
Pitfalls and forbidden moves
Adopt a pattern and then step on any of the following, and what you get is spaghetti with a pattern name on it. Here are the six most dangerous.
| Forbidden move | Why it is bad → what to do instead |
|---|---|
| God classes and God modules — thousands of lines, a dozen responsibilities | the textbook single-responsibility violation → split once it passes 300 lines |
| Circular dependencies (A → B → A) | they always breed bugs → detect them automatically with ESLint import/no-cycle or the equivalent |
| Reusing the ORM-generated entity across every layer | a database schema change propagates all the way to the UI → convert through a DTO at each boundary |
| Fat services — stuffing all the logic into the service layer | the entity degenerates into a plain data structure → move logic onto entities and value objects |
| Mocking your own code in tests | integration bugs stop being detectable → mock only the outside world, the database and external APIs |
| Applying Clean Architecture to a CRUD application | the effort does not match the business value → layered is enough |
Borrowing a pattern name without reading the source — Fowler’s PoEAA, Martin’s Clean Architecture — tends to produce an implementation that differs from what the original intended. Whichever pattern you pick, if the common principles hold — dependencies point one way, the domain is independent of UI and database, responsibilities are explicit — the name of the pattern is secondary.
Author’s note — “are we writing this for seven screens?”
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
Write your answers down as an ADR. A concrete guide to writing them is here.
Related Articles
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.
Also popular with readers
📚 Series: Architecture Crash Course for the Generative-AI Era (26/95)