Quarkus Insights #254: Domain Driven Design and Hexagonal Architecture - Part 2
Holly Cummins and Eric Deandrea welcomed be back to Quarkus Insights this week for Episode 254, a follow up conversation to Quarkus Insights Episode #248.
The topic is Domain Driven Design, Hexagonal Architecture, and scalable monoliths with Quarkus, and we touched on both of topics.
The code we walked through lives here: quarkus-insights-071326. There is a summary below, and don't hesitate to contact me if you have any questions or want to dig into these topics.
What it is
The code implements the Call for Papers subdomain of a larger Conference domain. If you haven't participated in a Call for Papers, it is how presenters submit sessions and conference organizers review them.
Quarkus is often thought of as a microservices runtime (the best one, IMHO), but is also a great runtime for monoliths.
The CLAUDE.md file is a decent tour of the architecture. I ~~wrote~~ prompted Claude Code to write it as a guideline for generating new subdomains, but it doubles as documentation for the codebase.
Running the app
You can run the app by cloning the repo and running mvn quarkus:dev from the root directory. You will need Docker or a Docker knockoff running becuase Quarkus' dev mode spins up the database in the background.
The application loads up 2 conferences which are defined in the import.sql. You can submit and review proposals and edit the CFPs at http://localhost:8080 after starting Quarkus.
mvn test runs the full suite, ArchUnit tests included.
Bounded contexts
The app is split into three bounded contexts:
cfp— the primary context. Proposals, sessions, presenters, tracks, formats. This is where most of the domain logic lives.communications— owns outbound messaging policy. It turns published CFP events into durableCommunicationrecords and retryable email deliveries.conference— you can ignore this one; it's only there because I will eventually build out the example some more.
An end-to-end walk through
In the video I walk through submitting a proposal to a conference submission looking at the following classes:
- SessionProposalParameters.java
- SessionPropsalResource.java
- CreateSessionProposalCommand.java
- CfpApplicationService.java
- SessionProposal.java
- SessionProposalRepository.java
- SessionProposalEntity.java
- SessionProposalMapper.java
- SessionProposalDTO.java
- SessionProposalAcceptedEvent.java
Those classes are (roughly) the workflow in order.
Package structure
The bounded context is split into functional packages:
domain/ # Pure domain — no Jakarta EE, no ORM, no REST
application/ # Use-case orchestration (*ApplicationService)
persistence/ # Panache repositories + JPA entities
infrastructure/ # JAX-RS resources
It is important to keep the parts of the application separate, but I'm not a stickler for package naming. I think it would be fine, for example, to put the REST Endpoints in a rest/ package.
Some things to note
- Aggregates use static factory methods instead of public setters.
- Value objects are immutable records with validation baked into their compact constructors.
- Commands are immutable records too.
- -Repositories own all the mapping between domain aggregates and JPA entities, so the domain layer never has to know a database exists.
ArchUnit
I'm also using ArchUnit tests to enforce layering, not just convention. There are eight tests in src/test/java/.../cfp/architecture/, checking things like:
- Domain classes can't import
jakarta.persistenceorjakarta.ws.rs - Aggregates can't have public fields or setters
- Service classes have to be named
*ApplicationServiceor*DomainService— a bare*Servicefails the build, because it usually means someone smeared orchestration logic and domain logic together - Cross-context access has to go through public
domain.servicesordomain.events
Watch it, please!
Please watch the recording for the full walkthrough.