Software Architecture

Choosing Module Design — Layered / Hexagonal / Clean

Choosing Module Design — Layered / Hexagonal / Clean

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.”

GoalEffect
Standardize where code livesNewcomers don’t get lost
Organize dependenciesPredictable change impact
Make testing easierEarly bug detection
Survive future changesTech-stack swaps possible

The four major patterns

Four widely used in practice. All share “controlling the direction of dependencies”:

PatternQuick trait
Layered ArchitectureHorizontal split: UI / business / data. The classic
Hexagonal ArchitectureDomain at the center, ports / adapters isolating external connections
Onion ArchitectureConcentric layers, domain at the innermost
Clean ArchitectureOnion’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.

StrengthsWeaknesses
Low learning costBusiness layer tends to depend on DB implementation
Every team is familiarMore layers = more thin pass-throughs
Natural fit with framework defaultsTests 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.

StrengthsWeaknesses
Easy testingOverspec at small scale
Easy to swap external dependenciesPort / adapter design skill required
Domain stays independent and protectedMore classes
Strong long-term operationMid-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;
LayerRole
Entities (innermost)Business rules; most stable
Use CasesBusiness flows; app-specific logic
Interface AdaptersDTO (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.

AspectLayeredHexagonalOnionClean
Learning cost◎ Low◯ Mid△ Mid-high△ High
Small-scale fit×
Testability
Change resilience
Initial costLowMidMid-highHigh
Suitable teamBeginner+Mid+Mid-experiencedExperienced

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 complexitySuitable design
Simple (CRUD-centric)Layered
ModerateHexagonal
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 lifetimeRecommended
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:

CodebaseTeam sizeOperating yearsRecommended patternFile-line target
Up to 10k LOC1-3up to 3 yrPlain MVC / Layered<= 300 lines
Up to 50k LOC3-103-10 yrLayered or Hexagonal<= 300 lines
Up to 200k LOC10-305-15 yrHexagonal or Clean<= 300 lines
200k LOC+30+10+ yrClean + 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 moveWhy
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 layersDB-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 implementationAbstraction without swap candidates is overspec. Abstract only when there are 2+ swap candidates
Mock your own code in testsMocking 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 originalImplementation diverges from intent; spaghetti with a pattern name
Apply Clean Architecture to a CRUD appOver-engineering that doesn’t pay for itself against business value. Layered is enough
Adopt DDD pattern names only without structuring domain vocabularyMimicking 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 favorableAI-era unfavorable
Small, self-contained modulesGod Module (giant, multi-responsibility)
Interfaces making dependency direction explicitImplicit dependencies, scope-crossing
Maintained type-definition files (.d.ts, etc.)Untyped dynamic calls
Standard patterns like Layered / CleanIdiosyncratic structures
  1. Narrow candidates by domain complexity (CRUD-centric or rich business rules).
  2. Pick a pattern matching team maturity (an unrunnable pattern is worthless).
  3. Justify initial investment by project lifetime.
  4. 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.