A reference boilerplate for Clean Architecture as described by Robert C. Martin, implemented with Spring Boot.
The goal is to demonstrate strict boundary enforcement across all four architectural rings, full Presenter-based output isolation, and a deliberate testing pyramid — including the rarely seen OutputBoundary pattern in practice.
The domain is intentionally minimal — a single User entity with full CRUD — so the architectural patterns remain in focus rather than getting buried in business details.
See also:
application-layered-sample— the same feature set as a classic layered monolithapplication-hexagonal-sample— the same feature set in Hexagonal (Ports & Adapters) styleapplication-onion-sample— the same feature set in Onion Architecture style
All four projects are designed to be read side by side. The same CRUD feature, four different architectural expressions of the same idea.
┌──────────────────────────────────────────────────────┐
│ Frameworks │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Interface Adapters │ │
│ │ │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Use Cases │ │ │
│ │ │ │ │ │
│ │ │ ┌──────────────────────────────┐ │ │ │
│ │ │ │ Entities │ │ │ │
│ │ │ └──────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
| Ring | Package | Responsibility |
|---|---|---|
| Entities | entities |
Core domain objects. User record, UserNotFoundException. Zero outward dependencies. |
| Use Cases | usecases |
Business rules. Input/Output boundaries, Gateways, Interactors, Request/Response models. Package-private implementations. |
| Interface Adapters | interfaceadapters |
Controllers, Presenters, DTOs. Translate between the web world and the use-case world. |
| Frameworks | frameworks |
Spring infrastructure. Persistence adapters (JDBC, jOOQ, JPA), ID generator, exception handlers. |
InputBoundary per operation. There is no single UserService. Each operation has its own interface: CreateUserInputBoundary, FindUserByIdInputBoundary, and so on. Controllers inject only what they actually need — a concrete expression of the Interface Segregation Principle.
OutputBoundary and Presenter. Query use-cases do not return data. Instead, they accept a UserOutputBoundary and call presenter.presentSingle() or presenter.presentList() — the use-case pushes data out rather than the controller pulling it. UserRestPresenter and UserMvcPresenter each implement the boundary and build the view model appropriate for their delivery mechanism. This is the defining characteristic of full Clean Architecture — the use-case is completely isolated from how its output is consumed.
Package-private interactors. CreateUserInteractor, FindUserByIdInteractor, and all other interactors are declared without public. They are wired as Spring beans through UseCaseConfig, which lives in the same package (usecases.interactor) and therefore has direct access. Nothing outside the package can instantiate or reference them directly.
Gateway separation. ReadRepositoryGateway, WriteRepositoryGateway, and IdGeneratorGateway are separate interfaces. Interactors depend only on what they use — read-only interactors have no reference to the write gateway.
Swappable persistence. Three complete implementations (JDBC via JdbcClient, jOOQ, JPA) are activated by Spring profile. Each implementation is package-private. Each lives alongside its own @Configuration class in the same package. Zero changes to the application core when switching.
@Transactional on interactors. Transaction boundaries are owned by the use-case layer, not the persistence layer. Read-only interactors use @Transactional(readOnly = true) — a deliberate optimization that reduces overhead at both the JDBC driver and ORM levels.
JPA entities never leak out. UserJpaEntity is package-private. A dedicated UserJpaMapper converts between the JPA entity and the domain User in both directions. The boundary between the persistence model and the domain is drawn explicitly.
Controllers build RequestModels directly. There is no mapper for incoming data. The controller constructs CreateUserRequestModel and UpdateUserRequestModel inline from the HTTP DTO — this is exactly the role Martin assigns to the controller. The Presenter handles all outgoing formatting.
Two UIs, one core. REST controllers and MVC controllers (JTE templates) are both driving adapters calling the same use-cases through the same boundaries. A concrete proof that the delivery mechanism has no bearing on business logic.
Separate error handling. GlobalRestExceptionHandler and GlobalMvcExceptionHandler are each scoped to their own package. REST returns RFC 9457 Problem Details; MVC renders a template with an error message. Context-appropriate handling, not a catch-all.
src/main/java/.../
├── entities/
│ ├── User # Core domain record
│ └── UserNotFoundException # Domain exception
│
├── usecases/
│ ├── boundaries/
│ │ ├── in/ # InputBoundary interfaces (one per operation)
│ │ └── out/ # UserOutputBoundary
│ ├── gateway/ # ReadRepositoryGateway, WriteRepositoryGateway, IdGeneratorGateway
│ ├── interactor/ # Package-private interactors + UseCaseConfig
│ ├── requestmodel/ # CreateUserRequestModel, UpdateUserRequestModel
│ └── responsemodel/ # UserResponseModel
│
├── interfaceadapters/
│ ├── controller/
│ │ ├── rest/ # RestQueryController, RestCommandController
│ │ └── mvc/ # MvcQueryController, MvcFormController, MvcCommandController
│ ├── presenter/ # UserRestPresenter, UserMvcPresenter
│ └── dto/ # UserResponse, CreateUserRequestDto, UpdateUserRequestDto
│
└── frameworks/
├── persistence/
│ ├── jdbc/ # JdbcUserReadGateway, JdbcUserWriteGateway, JdbcConfig
│ ├── jooq/ # JooqUserReadGateway, JooqUserWriteGateway, JooqConfig
│ ├── jpa/ # JpaUserReadGateway, JpaUserWriteGateway,
│ │ # UserJpaEntity, UserJpaMapper, UserJpaRepository, JpaConfig
│ └── id/ # UuidV7IdGenerator, IdConfig
└── web/
├── GlobalRestExceptionHandler
└── GlobalMvcExceptionHandler
# Default — jdbc profile, H2 in-memory, port 8083
./mvnw spring-boot:run
# Switch to jOOQ
./mvnw spring-boot:run -Dspring-boot.run.profiles=jooq
# Switch to JPA
./mvnw spring-boot:run -Dspring-boot.run.profiles=jpaThe H2 console is available at http://localhost:8083/h2.
| Method | URL | Description |
|---|---|---|
| GET | /api/users |
List all users |
| GET | /api/users?namePrefix=Al |
Filter by name prefix (case-insensitive) |
| GET | /api/users/{id} |
Get user by ID |
| POST | /api/users |
Create a user (Location header in response) |
| PUT | /api/users/{id} |
Update a user |
| DELETE | /api/users/{id} |
Delete a user |
Errors are returned as RFC 9457 Problem Details.
Entry point: http://localhost:8083/mvc/users
The MVC interface exists to demonstrate dual-presentation — two UIs on top of a single application layer, each with its own Presenter. A UUID is an internal system identifier and is never meant to be exposed to the user; the MVC search-by-ID endpoint exists solely for demonstration.
The testing pyramid is structured deliberately — each level has a distinct purpose:
[E2E] RestTestClient, random real HTTP port
[Integration] @SpringBootTest + MockMvc + H2 + @Sql
[Slice] @WebMvcTest — controllers in full isolation
[Unit] Interactors via Fake gateways, no Mockito
[Arch] ArchUnit — dependency rules enforced on bytecode
Fake gateways over Mockito in interactor unit tests. FakeReadRepositoryGateway and FakeWriteRepositoryGateway provide real in-memory behavior. Tests verify logic rather than asserting method calls.
FakeOutputBoundary in query interactor tests. The same pattern applies to the OutputBoundary — a lightweight fake captures what the interactor pushed, allowing assertions without Mockito.
doAnswer in slice tests for query controllers. Because query use-cases are void and push data through the Presenter, slice tests use doAnswer to simulate the interactor calling presenter.presentSingle() or presenter.presentList(). This is the cost of the full Presenter pattern — slightly more ceremony in tests, complete isolation in production code.
@Sql with separate files. test-schema.sql and test-data.sql are decoupled. Command integration tests use only the schema; query integration tests also load fixtures. Each test gets exactly the state it needs.
RestTestClient in E2E. A dedicated client over a real HTTP port, not MockMvc. MockMvc exercises the Spring MVC dispatch pipeline; RestTestClient goes through the full network stack.
ArchUnit rules verify the dependency direction on every build:
- Entities depend on nothing
- Use cases do not depend on Interface Adapters or Frameworks
- Interface Adapters do not depend on Frameworks
- Controllers do not depend on Interactors directly — only through InputBoundary interfaces
./mvnw test- Java 21, Spring Boot 3.x
- H2 (in-memory, development and tests only)
- Spring Web MVC — REST and HTML controllers
- Spring JdbcClient (6.1+), jOOQ, Spring Data JPA — three interchangeable adapters
- JTE — template engine for MVC views
com.fasterxml.uuid— UUID v7 generation- ArchUnit — architectural constraint tests