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