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

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

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 main 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 — 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.

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

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

Clean Architecture Concentric Circles and Dependency Direction Dependencies flow outward → inward one-way. Inner layers don't know about outer layers Frameworks & Drivers DB, Web, UI Frameworks Express PostgreSQL React Redis Interface Adapters Controller, Presenter, DTO Conversion Use Cases Business flow, app-specific logic Entities Business Rules Most stable Dependency Direction Outer → Inner only Dependency Inversion Principle (DIP) Entities (Innermost Layer) Business rules. Depends on nothing Use Cases Business flow. References only Entities Interface Adapters DTO conversion. Implements inner interfaces Frameworks & Drivers DB & FW. Most likely to change UI/DB changes don't affect business rules. DDD + Clean is the go-to for long-term operations
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.

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.

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.

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

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:

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.

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.

Personal / Startup: Ship in One Month Is Correcten.senkohome.com/arch-intro-case-startup/

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.

Small-Mid SaaS - Lean on Managed and Run with Few Peopleen.senkohome.com/arch-intro-case-saas/

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.

Large-Enterprise Core: Design That Holds Up for Yearsen.senkohome.com/arch-intro-case-enterprise/

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.

Controlling Dependency Direction via Interfaces Inner layers define interfaces, outer layers implement. The key to inverting dependencies NG: direct ref. Domain Layer Business Logic import DB Connection PostgreSQL Issues Domain directly depends on DB DB change → Business logic also needs modification Tests require DB connection AI ignores boundaries and runs wild OK: Via Interface Domain Layer Business Logic interface Definition is inside DB Connection PostgreSQL implements Effect Domain doesn't know about DB DB changes don't propagate to business logic Mocks can be swapped in tests AI generates accurately following types Explicit dependency direction becomes AI's guardrails. Circular deps detected immediately by lint

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.

Module Design Pattern Comparison Criteria Layered Hexagonal Onion Clean Learning Curve ◎ Low ○ Medium △ Medium-High △ High Small-Scale Fitness × Testability Change Resilience Initial Cost Low Medium Medium-High High Suitable Team Beginners+ Mid-level+ Mid-level to Expert Expert

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 moveWhy it is bad → what to do instead
God classes and God modules — thousands of lines, a dozen responsibilitiesthe 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 layera 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 layerthe entity degenerates into a plain data structure → move logic onto entities and value objects
Mocking your own code in testsintegration bugs stop being detectable → mock only the outside world, the database and external APIs
Applying Clean Architecture to a CRUD applicationthe 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.

[DevOps Architecture] Documentationen.senkohome.com/arch-intro-devops-docs/

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.