Clean Architecture
Structure a Java application so its business rules stay pure and testable — by splitting it into layers, pointing every dependency inward, and hiding databases and frameworks behind ports and adapters.
Learn Clean Architecture in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
Before You Start
This lesson builds on Interfaces & Abstraction (ports are interfaces), OOP Design Patterns (dependency injection), and Unit Testing (the payoff is testable code). Comfort with Generics helps too.
A Real-World Analogy
Think of your application as a house with standard wall sockets . The wiring inside the wall is your domain — the real logic, hidden and stable. A socket is a port : a fixed, agreed-upon shape that says "plug something in here." A lamp, a kettle, or a phone charger is an adapter : it conforms to the socket and provides the actual behaviour.
You can swap the kettle for a toaster without rewiring the house, because both speak "socket." In the same way you can swap a JpaOrderRepository for an in-memory fake, or Stripe for PayPal, without touching a single business rule — as long as they plug into the same port. The wiring (domain) never reaches out to grab a specific appliance; the appliances plug into it. That is the dependency rule made physical.
1️⃣ The Four Layers & the Dependency Rule
Clean Architecture sorts your code into concentric layers — picture an onion . At the centre is the domain : pure business logic that knows nothing about databases, the web, or any framework. Around it sits the application layer (your use cases). On the outside live infrastructure (databases, message queues, third-party APIs) and presentation (controllers, DTOs).
One rule holds the whole thing together — the dependency rule : source-code dependencies may only point inward . An outer layer may import an inner one, but an inner layer must never import an outer one. Your domain therefore never mentions Spring or JPA. That is what lets you test business logic with no framework, swap your database without rewriting rules, and read the domain to understand the system.
Layer
Contains
May Depend On
Example
Domain
Entities, Value Objects, Events
Nothing!
Order, Money, OrderId
Application
Use cases, Ports (interfaces)
Domain only
PlaceOrderUseCase
Infrastructure
DB repos, API clients, messaging
Application + Domain
JpaOrderRepository
Presentation
Controllers, DTOs, view models
OrderController
2️⃣ The Domain: Entities & Value Objects
The domain layer holds two kinds of object. An entity has a stable identity that outlives its data: two Order s with the same OrderId are the same order, even if their contents differ — so you compare them by id. A value object has no identity; it is defined entirely by its values, so two Money of USD 10.00 are interchangeable and equal.
Crucially, these objects carry their own behaviour and rules . Order.complete() refuses to run twice; Money.add() rejects mismatched currencies. An object that is just fields with getters and setters — with all the logic living in some service — is called an anaemic domain model , and it is exactly what Clean Architecture avoids. Put the rules in the object that owns the data.
3️⃣ Ports, Use Cases & Dependency Inversion
The application layer answers "what can this system do ?" Each capability is a use case (also called an interactor ) — one class that orchestrates a single workflow, like "place an order." It builds domain objects, calls their methods, and coordinates the steps. It contains orchestration, not business rules — those live in the domain.
A use case needs a database and a payment gateway, but it must not know about them. So it declares ports : plain interfaces it owns. An input port is the use case interface the outside world may call ( PlaceOrderUseCase ). An output port is a capability the use case needs from the world ( OrderRepository , PaymentPort ). The use case depends only on these interfaces.
This is the Dependency Inversion Principle : instead of the high-level use case depending on a low-level JpaOrderRepository , both depend on the OrderRepository abstraction. The use case receives a concrete implementation through its constructor (constructor injection) — it never writes new JpaOrderRepository() . The actual wiring happens once, at startup, in a configuration class.
🎯 Your Turn #1 — Point the Arrows Inward
4️⃣ Adapters & Enforcing the Rule
An adapter is an outer-layer class that implements a port . JpaOrderRepository implements OrderRepository ; StripePaymentAdapter implements PaymentPort . This is the only place framework details — @Repository , JPA mapping, the Stripe SDK — are allowed to appear. At its edges, an adapter maps between the outside shape (a database row, a JSON body) and the domain object, so neither side leaks into the other.
The dependency rule is easy to state and easy to break by accident. The fix is to make it a test . ArchUnit lets you assert architecture in plain JUnit: "no class in ..domain.. may depend on ..infra.. ." Add it to your suite and mvn test goes red the moment someone imports the wrong package — a guardrail, not a guideline on a wiki.
🎯 Your Turn #2 — Add a Port & an Adapter
5️⃣ A Package Structure That Makes the Rule Obvious
Pick a package layout where the layers are visible at a glance, so a wrong dependency stands out in a code review (and ArchUnit catches the rest). Group by layer first , then by feature inside. Read it inside-out: domain has no dependencies, application depends only on the domain, and the outer folders hold the adapters.
Common Errors (and the Fix)
- ❌ Domain depending on a framework — your Order imports org.springframework or a JPA @Entity , so the domain can no longer be tested or reused without that framework. Fix: keep the domain pure Java; let an outer adapter map to/from persistence types at the boundary.
- ❌ Anaemic domain model — entities are bags of getters/setters and every rule lives in a fat service. Fix: move the behaviour onto the object that owns the data — order.complete() , money.add() — so invariants are enforced where the state lives.
- ❌ Leaking infrastructure into the domain — a JPA Order entity (with @Column , lazy proxies, a no-arg constructor) is passed around as if it were the domain object. Fix: use a separate persistence entity and a mapper; the domain object stays clean.
- ❌ Circular dependencies between layers — the use case calls the adapter and the adapter calls back into the use case, so neither compiles in isolation. Fix: depend on a port interface in one direction only; introduce a domain event if the outer layer needs to react.
- ❌ Newing dependencies inside a use case — new JpaOrderRepository() inside the service hard-wires it to one database and breaks dependency inversion. Fix: take the OrderRepository port through the constructor and wire the concrete adapter once, in configuration.
Pro Tips
- 💡 Records for value objects — record Money(BigDecimal amount, Currency currency) {} gives you immutability and value-equality for free, exactly what a value object needs.
- 💡 Enforce architecture with ArchUnit — a single test that says "domain must not depend on infra" turns the dependency rule into a build failure, not a hope.
- 💡 Prefer domain events over direct coupling — instead of the use case calling an EmailService directly, publish an OrderPlaced event and let a handler react. The outer layer plugs in without the inner layer knowing.
- 💡 Start simple, grow deliberately — begin with a rich domain plus repository ports; add use cases, more value objects, and events only as the business rules actually multiply.
📋 Quick Reference
Concept
In Java
Purpose
Dependency rule
imports point inward only
Domain stays framework-free
Entity
class Order (has OrderId)
Identity + behaviour
Value object
record Money(amount, currency)
Immutable, equal by value
Input port
interface PlaceOrderUseCase
What the outside may call
Output port
interface OrderRepository
What the app needs (WHAT)
Adapter
JpaOrderRepository implements
Concrete implementation (HOW)
Use case / interactor
class PlaceOrderService
Orchestrates one workflow
Dependency inversion
constructor takes the port
Both depend on the interface
Domain event
record OrderPlaced(OrderId)
Decoupled cross-boundary signal
Frequently Asked Questions
Mini-Challenge — A Cancel-Order Use Case
🎉 Lesson Complete!
You can now structure a Java application the clean way: a pure domain of entities and value objects that own their rules, an application layer of use cases that orchestrate workflows through ports , and outer adapters that implement those ports behind frameworks. The dependency rule keeps every arrow pointing inward, dependency inversion keeps Spring and JPA out of your business logic, and an ArchUnit test keeps it that way for good.
Next up: the Final Project — build a complete Java application that applies everything you've learned across the course.
Practice quiz
The dependency rule states that source-code dependencies may only point:
- Outward, toward infrastructure
- In any direction
- Inward, toward higher-level policy (the domain)
- From domain to presentation
Answer: Inward, toward higher-level policy (the domain). Outer layers may import inner ones, never the reverse. The domain depends on nothing.
Which layer is allowed to depend on NOTHING (no framework, no other layer)?
- Domain
- Presentation
- Infrastructure
- Application
Answer: Domain. The domain is the pure core — it knows nothing about Spring, JPA, HTTP, or any outer layer.
A Money value object compares as equal when:
- Its memory address matches
- It has the same id
- Never — value objects are always distinct
- Its amount and currency are equal (value equality)
Answer: Its amount and currency are equal (value equality). A value object has no identity; two Money of USD 109.98 are equal by value. Records give this for free.
How does an Entity differ from a Value Object?
- An entity is always immutable
- An entity has a stable identity that outlives its data
- A value object has an id field
- They are the same thing
Answer: An entity has a stable identity that outlives its data. Entities (like Order with an OrderId) are compared by identity; value objects (like Money) by their values.
In Ports & Adapters, a 'port' is:
- An interface the application owns (e.g. OrderRepository, PaymentPort)
- A concrete database class
- A network socket
- A Spring annotation
Answer: An interface the application owns (e.g. OrderRepository, PaymentPort). A port is an interface the app defines. An adapter (JpaOrderRepository, StripePaymentAdapter) implements it.
Where does the OrderRepository interface live?
- In the infrastructure layer
- In the presentation layer
- In the application layer, next to the use case that needs it
- Outside the project
Answer: In the application layer, next to the use case that needs it. The output port sits in the application layer; the outer adapter reaches in to implement it — that inversion keeps arrows inward.
A use case (interactor) should contain:
- All the business rules
- Orchestration of one workflow, delegating rules to the domain
- JPA mappings
- HTTP controllers
Answer: Orchestration of one workflow, delegating rules to the domain. A use case coordinates steps and calls domain objects; the actual business rules live in the domain entities.
Dependency inversion in a use case means the service:
- Calls new JpaOrderRepository() directly
- Extends the adapter class
- Stores a static reference to the database
- Receives a port implementation through its constructor
Answer: Receives a port implementation through its constructor. The service depends on the abstraction and gets the concrete adapter via constructor injection, wired at startup.
An 'anaemic domain model' is one where:
- Entities own their rules and behaviour
- Entities are just getters/setters and all logic lives in fat services
- There are too many value objects
- The domain depends on no framework
Answer: Entities are just getters/setters and all logic lives in fat services. Clean Architecture avoids anaemic models: put behaviour (order.complete(), money.add()) on the object that owns the data.
What does an ArchUnit test let you do?
- Generate adapters automatically
- Speed up the build
- Turn the dependency rule into a failing test if a layer imports the wrong package
- Replace unit tests
Answer: Turn the dependency rule into a failing test if a layer imports the wrong package. ArchUnit asserts architecture in JUnit, so 'mvn test' goes red the moment someone breaks the dependency rule.
Continue this course
- Previous: Previous Lesson
- Next: Next Lesson