I wanted to write a simple-ish Spring REST application to experiment with these technologies:
- Java 21
- Spring Boot 4.x
- Spring Data JPA
- Hibernate 7
- H2 (test database)
- AssertJ
… and these aims:
- non-anemic domain model using aggregates
- separate Command and Query operations
- sensible tests aimed at specific layers of the application
The domain looks like this:

The application is broken down into layers.
- Domain / JPA Repository Layer
- Application Service Layer
- REST Controller Layer

I will briefly summarise each of these layers, and point to where the important code and tests are.
Domain / JPA Layer
The main classes in this layer are:
A focus was to avoid an anemic domain model by using aggregates:
An aggregate is a cluster of associated objects treated as a single unit for data changes, with a clearly defined boundary and a root entity that enforces invariants.
— Eric Evans, Domain-Driven Design
Our application has 2 aggregates, Customer and Tag. In this first iteration I am going to focus on the Customer aggregate:

The basic idea is that by grouping entities into logical groups (called an aggregate) and controlling access to this group through a single entity (called the aggregate root) we can create better systems.
Enforcement of invariants becomes simpler, because it happens inside the aggregate, rather than leaking into services. And since all traffic to the aggregate goes via the aggregate root, its easier to reason over. In fact, the aggregate root never returns entity objects from within the aggregate.
For example, take Customer, our aggregate root. It has no entity objects as return values. And it is the only entity in the aggregate with public methods - neither Ticket or Profile has any.
Also note that all the entities in the aggregate focus on domain behaviour (like resolveTicket()) rather than setters (like setStatus('resolved')).
Domain tests for the sample application include unit tests that run without any JPA context. And @DataJpaTest tests:
Application Service Layer
The main classes in this layer are:
It can be useful to separate the processes that query a system from the processes that change it. That way we can implement different approaches (or even different models) for the query and the mutate parts. This is the basis of the generic Object Oriented pattern Command Query Separation, and the more involved Command Query Responsibility Segregation (CQRS).
The application services are split between CustomerCommandService and CustomerQueryService.
CustomerCommandService talks directly to the Customer aggregate root to apply changes to the data using Command objects. One outcome of this approach is you can end up with a lot of Command objects, since the recomendation is usually to use one object per command. These simple data carriers can be represented with records for convenience.
CustomerQueryService uses its CustomerQueryRepository and JPQL, avoiding the domain model entirely. The Query side returns projections in the form of records.
Tests for this layer can be found in:
REST Controller Layer
The main classes in this layer are:
This is a vanilla Spring REST controller layer. Its thin, uses DTOs for requests, and basically calls the the Application Layer.
Tests for this layer can be found in:
Other bits (deployment and a little load testing for sanity)
Apart from the Layers its worth mentioning the application has a few other features:
Seeder
The seeder classes are:
The SeedCommandLineRunner runs DemoDataSeederService when the Spring profile is “seed”.
Docker
The Docker classes are:
Running the following will build and seed the application:
docker-compose up --build --detach
Then provided K6 is installed, you can run some basic load tests:
k6 run customer-write-smoke-test.js
To just run the application and Postgres (no seeding):
docker-compose -f docker-compose-no-seed.yaml up --build --detach
GitHub build script
I have included a GitHub Actions script to publish the container image produced by the Spring Demo application. It is published to GitHub Container Registry
Resources
- Domain-Driven Design Reference - Eric Evans
- Domain-Driven Design: Reference Definitions and Pattern Summaries - Eric Evans
- Command Query Separation pattern - Martin Fowler
- Command Query Responsibility Segregation (CQRS) - Martin Fowler
- Getting started with CQRS
- Services in domain Driven Design
- Domain-Driven Design: Entities, Value Objects, and How To Distinguish Them
- K6
- Chart Releaser Action to Automate GitHub Page Charts
- Publishing Helm Charts to GitHub Container Registry