Start Learning
Javaneer
Back to stage
Stage 3·Hexagonal & Clean Architecture

Is It Worth It?

When full hexagonal pays off versus a simpler layered design, the middle grounds, and how you'd refactor ledger-legacy toward the domain-at-the-center shape incrementally.

13 min readIntermediate
On this page

This module has sold hexagonal architecture hard, so it must close honestly: it is not the right choice for every app. The purity, ports, adapters, and mapping are an investment that pays back in some codebases and is pure overhead in others. A senior engineer can articulate when the structure earns its keep, knows the middle grounds between "full hexagonal" and "big ball of mud," and can refactor toward it incrementally rather than demanding a rewrite.

The cost, stated plainly

Full hexagonal architecture costs real complexity:

  • More classes and indirection - ports, adapters, separate domain and persistence models, mapping code. A feature that's one annotated class in a simple app becomes several.
  • A learning curve - the team must understand the Dependency Rule and resist the natural pull to just call the database from a service.
  • Ceremony that dwarfs trivial logic - wrapping a CRUD form in ports and use cases adds layers over logic that's just "save this row."

If the app is essentially forms over data, this structure buys almost nothing and slows everyone down. The "is it worth it?" question is the same one you asked of DDD and design patterns - because it's the same judgment.

When it pays off

Reach for hexagonal/clean architecture when:

  • The domain logic is rich and central - complex rules worth isolating and testing in pure isolation (banking, insurance, trading, logistics). This is the dominant signal.
  • The app is long-lived - it'll outlast several framework versions and maybe a database migration, so independence from those pays compounding dividends.
  • You value fast, thorough testing of business logic without infrastructure - the millisecond unit tests are a daily payoff.
  • Multiple delivery mechanisms or integrations exist - the same core serving REST, a CLI, a message consumer, and scheduled jobs benefits directly from driving-side ports.

Skip most of it for CRUD apps, prototypes, short-lived tools, and services whose hard part is technical (raw throughput), not domain complexity.

The middle grounds

It's not binary. Between a big ball of mud and full ports-and-adapters lie pragmatic points on a spectrum:

  • Keep the Dependency Rule, relax the mapping. Point dependencies inward and use domain-owned repository interfaces, but let a simple module reuse one model instead of maintaining separate domain and persistence classes. You get testability and inversion without all the boilerplate.
  • Apply it per bounded context. Use full hexagonal for the complex core context (the underwriting engine) and plain layered CRUD for the simple ones (admin screens). Architecture need not be uniform across a system.
  • Extract the domain first. The highest-value move is often just pulling business logic out of controllers and framework-coupled services into a pure core with repository interfaces - most of the benefit, a fraction of the ceremony.

Refactoring ledger-legacy incrementally

You'd never stop to rewrite ledger-legacy. You'd evolve it, exactly as this whole path preaches:

  1. Extract a pure domain model from the annotated entities (a plain Account beside AccountEntity).
  2. Introduce repository ports the domain owns, implemented by the existing JPA code as adapters (Dependency Inversion - the arrow flips).
  3. Pull orchestration into use cases, draining the fat LedgerManager/services.
  4. Push framework code to the edges - controllers become thin driving adapters mapping HTTP to use cases.
  5. Add fast unit tests against the now-pure core as you go, locking in each improvement.

Each step is independently valuable and shippable; you can stop at any point where the return no longer justifies the effort. That optionality - improve until it's good enough, not until a diagram is satisfied - is the mark of architectural judgment.

Architecture is a means, not a trophy

The failure mode isn't only under-engineering (the mud ball); it's over-engineering - imposing full hexagonal on a to-do app to feel rigorous, then drowning in indirection. Neither extreme is 'senior.' The senior move is matching the structure to the problem's actual complexity and longevity, and being able to defend the choice either way. 'We used hexagonal' is not an achievement; 'this system is easy to change where change actually happens' is.

Foundations sized to the building

You pour deep, reinforced, engineered foundations for a skyscraper - skip them and it collapses. You pour a simple slab for a garden shed - over-engineer that with skyscraper foundations and you've wasted a fortune and months for a shed. Hexagonal architecture is deep foundations: essential under a complex, long-lived, load-bearing system; absurd under a weekend tool. And crucially, you can underpin an existing building - adding foundations room by room (the incremental refactor) - without demolishing it. The engineer's skill is reading how much building is going on top before deciding how much foundation to pour.

Right-size the architecture

Three services need architectural decisions. (1) A greenfield core lending-decision engine: complex rules, central to the business, a 5+ year lifespan, and it'll be driven by both a REST API and a batch job. (2) An internal CRUD tool to manage a lookup table of branch office details. (3) An existing, tangled but critical billing service that's painful to change and untested. Recommend an approach for each.

When is full hexagonal/clean architecture most worth its cost?

Key takeaways

  • Full hexagonal architecture costs real complexity - ports, adapters, dual models, mapping - justified only by domain complexity and longevity.
  • It pays off for rich, central, long-lived domains, when you value infrastructure-free testing, and when multiple delivery mechanisms share one core.
  • Skip most of it for CRUD apps, prototypes, and services whose hard part is technical throughput rather than domain complexity.
  • The middle grounds matter: keep the Dependency Rule but relax mapping, apply it per bounded context, or just extract a pure domain from the framework.
  • Refactor legacy toward it incrementally - extract domain, introduce ports, pull out use cases, thin the controllers - stopping when returns no longer justify effort.
Was this lesson helpful?
Edit this page on GitHub