Quarkus Insights #248: Introduction to Domain-Driven Design and Hexagonal Architecture

Wait 5 sec.

This summary was generated using AI, reviewed by humans - watch the video for the full story.Quarkus Insights #248: Introduction to Domain-Driven Design and Hexagonal ArchitectureIn episode 248 of Quarkus Insights, Jeremy Davis, a Solutions Architect with extensive experience in the JBoss ecosystem, provided an in-depth introduction to Domain-Driven Design (DDD) and Hexagonal Architecture using Quarkus.Why DDD is Having a MomentJeremy opened by noting that DDD seems to be experiencing renewed interest, with multiple presentation requests recently. He theorizes this resurgence relates to the rise of AI-assisted development and agentic systems, where DDD’s structured approach to organizing business logic makes it easier for AI tools to understand and generate code.The Core Principles of Domain-Driven DesignUbiquitous LanguageOne of Jeremy’s favorite aspects of DDD is the emphasis on ubiquitous language - a shared vocabulary between developers and business stakeholders. He illustrated this with the restaurant industry example: "86 ketchup" means "we’re out of ketchup" to anyone who’s worked in restaurants, but is completely nonsensical to outsiders.Since every industry has its own language, DDD encourages teams to:Identify and document domain-specific terminologyUse that language consistently in code, conversations, and documentationRefine the language iteratively with each sprint or development cycleDomains and SubdomainsFor the demo, Jeremy built an application for scheduling Quarkus Insights episodes, with several subdomains:Programming: Managing episode content and schedulingPeople: Managing hosts, presenters, and guestsEngagement: Handling viewer interactions and notificationsEach subdomain represents a distinct area of business concern with its own models and logic.Building Blocks of DDDAggregatesAggregates are the core business concepts that encapsulate business rules (invariants). In Jeremy’s example, the EpisodeAggregate serves as the root entity:public class EpisodeAggregate { private EpisodeId id; private EpisodeTitle title; private String description; private EpisodeAirDate airDate; private Collection domainEvents; public static EpisodeAggregate schedule( EpisodeTitle title, String description, EpisodeAirDate airDate) { // Business logic here EpisodeAggregate episode = new EpisodeAggregate(); episode.domainEvents.add(new EpisodeScheduledEvent()); return episode; }}Key characteristics:Aggregates are POJOs with no framework dependenciesAll business logic lives in the aggregateAggregates maintain invariants (rules that must always be true)You interact with the object graph through the aggregate rootValue ObjectsValue objects encapsulate data with no identity. That is, a value object is defined entirely by its attributes, not by some unique ID. Examples include addresses, email addresses, or in Jeremy’s demo, episode titles:public record EpisodeTitle(String value) { public EpisodeTitle { if (value == null || value.isBlank()) { throw new IllegalArgumentException("Episode title cannot be null or blank"); } // Could add more validation: // - Must contain "Quarkus" // - Must contain "Insights" // - Must have episode number }}Value objects enable validation at creation time and make the domain model more expressive.Domain EventsDomain events represent facts that the business cares about:public record EpisodeScheduledEvent( EpisodeId episodeId, EpisodeTitle title, LocalDate airDate) implements DomainEvent {}Events are:Statements of fact (they have occurred)ImmutableCan be replayedEnable event-driven architectureUnlike commands (which can be rejected), events represent things that have already happened.Hexagonal ArchitectureThe Layered ApproachJeremy demonstrated how to structure a DDD application using hexagonal architecture:Infrastructure Layer (Adapters):- REST endpoints (incoming port)- Database repositories (outgoing port)- Message brokers (outgoing port)- DTOs for wire formatApplication Layer:- Application services that orchestrate business logic- Command handlers- Domain services for logic that doesn’t fit in aggregatesDomain Layer:- Aggregates- Value objects- Domain events- Repository interfaces (not implementations)Application ServicesApplication services orchestrate the workflow:@ApplicationScopedpublic class EpisodeApplicationService { @Inject EpisodeRepository repository; @Inject DomainEventPublisher eventPublisher; public EpisodeDTO scheduleEpisode(ScheduleEpisodeCommand command) { // 1. Validate title doesn't exist if (repository.titleExists(command.title())) { throw new IllegalArgumentException("Title already exists"); } // 2. Create aggregate EpisodeAggregate episode = EpisodeAggregate.schedule( command.title(), command.description(), command.airDate() ); // 3. Persist episode = repository.persist(episode); // 4. Publish events episode.getDomainEvents().forEach(eventPublisher::publish); return toDTO(episode); }}Separating ConcernsA critical aspect of hexagonal architecture is keeping the domain pure:Aggregates have no knowledge of persistence frameworksRepositories handle the translation between aggregates and entitiesMappers convert between domain objects and database entities@ApplicationScopedpublic class EpisodeRepository implements PanacheRepository { public EpisodeAggregate persist(EpisodeAggregate aggregate) { EpisodeEntity entity = toEntity(aggregate); persist(entity); return rehydrate(entity); } public static EpisodeAggregate rehydrate(EpisodeEntity entity) { return new EpisodeAggregate( entity.id, new EpisodeTitle(entity.title), entity.description, new EpisodeAirDate(entity.airDate) ); }}The Trade-offsMore Code, Better OrganizationJeremy was upfront about the main drawback: you’ll write significantly more code with DDD compared to a simple CRUD application. The demo showed:Value objects for each domain conceptSeparate DTOs for REST endpointsCommands for operationsEvents for notificationsMappers between layersThe BenefitsHowever, this additional code provides substantial benefits:1. Maintainability: When inheriting code, business logic is easy to find - it’s all in the aggregates.2. Testability: Aggregates are POJOs that can be tested with plain JUnit, no framework required.3. Flexibility: Swapping persistence frameworks (e.g., from Hibernate to Firebase) only requires changing the repository implementation.4. Clear boundaries: Logic doesn’t leak across layers or get scattered in event handlers.5. AI-friendly: The structured approach makes it easier for AI tools to understand and generate code.Collaboration Between DomainsJeremy addressed a key question: how do domains interact?Close Collaboration PatternWhen teams work closely together:// People subdomain exposes an APIpublic interface PeopleAPI { Collection registerHosts(Collection hosts); Collection registerPresenters(Collection presenters);}// Episodes subdomain uses it via a domain service@ApplicationScopedpublic class PeopleDomainService { @Inject PeopleAPI peopleAPI; public Collection registerHosts(Collection hosts) { return peopleAPI.registerHosts(hosts); }}Anti-Corruption LayerWhen integrating with external systems you don’t control, use an anti-corruption layer to translate between their model and yours, protecting your domain from external changes.Transaction BoundariesImportant: Transaction boundaries should stay within each domain. If operations span multiple domains, consider:Using eventual consistency with domain eventsImplementing saga patterns for distributed transactionsCarefully evaluating if you’ve drawn domain boundaries correctlyValidation at Multiple LayersJeremy emphasized that validation occurs at different layers for different purposes:REST Layer (DTOs): Basic validation (not null, not blank)public record CreateEpisodeRequest( @NotBlank String title, @NotBlank String description, @NotNull LocalDate airDate) {}Domain Layer (Value Objects): Business rule validationpublic record EpisodeAirDate(LocalDate value) { public EpisodeAirDate { if (value.isBefore(LocalDate.now())) { throw new IllegalArgumentException("Episode cannot air in the past"); } }}Application Layer: Cross-aggregate validationif (repository.titleExists(title)) { throw new IllegalArgumentException("Title already exists");}AI-Assisted DDD DevelopmentJeremy demonstrated using Claude AI to generate DDD boilerplate:Skills and SpecificationsHe created:Quarkus skills: Instructions for using Panache, REST endpoints, loggingDDD skills: Patterns for aggregates, value objects, repositoriesSpecification files: High-level requirements for the applicationResultsUsing AI with these skills, Jeremy generated a complete DDD application structure in "YOLO mode" (letting the AI run freely). While not perfect (it didn’t use Panache as instructed), it created:Proper aggregate structureValue objects with validationRepository interfacesDomain eventsApplication servicesThe key insight: DDD’s structured, boilerplate-heavy nature makes it ideal for AI generation, while keeping business logic centralized for human review.Testing Architecture with ArchUnitJeremy mentioned ArchUnit, a library for testing architectural rules:@Testpublic void domainLayerShouldNotDependOnInfrastructure() { classes() .that().resideInPackage("..domain..") .should().onlyDependOnClassesThat() .resideInAnyPackage("..domain..", "java..") .check(importedClasses);}This helps enforce architectural boundaries automatically.When to Use DDDJeremy’s guidance on when DDD makes sense:Good fit:- Complex business logic- Long-lived applications- Multiple teams working on different domains- Need for clear boundaries and maintainability- AI-assisted development workflowsOverkill:- Simple CRUD applications- Prototypes or short-lived projects- Small teams with simple requirementsResources and Next StepsJeremy offered to return for follow-up episodes covering:State distribution across domainsEvent sourcing and saga patternsCQRS for read modelsAI-assisted DDD workflows in depthHe also mentioned:Presenting at Domain-Driven Design EuropeSpeaking at Explore DDD in ColoradoHis blog at The Arrogant ProgrammerKey Takeaways for DevelopersDDD provides structure that makes business logic easy to find and maintainHexagonal architecture keeps your domain pure and testableMore code upfront pays dividends in maintainabilityUbiquitous language bridges the gap between business and technologyValue objects enable validation at creation timeDomain events enable event-driven architecture naturallyAI tools work well with DDD’s structured approachTransaction boundaries should stay within domainsValidation happens at multiple layers for different purposesNot every application needs DDD - evaluate complexity firstConclusionDomain-Driven Design and Hexagonal Architecture provide powerful patterns for organizing complex business logic in Quarkus applications. While they require more upfront code, the benefits in maintainability, testability, and clarity make them valuable for long-lived, complex applications. The structured nature of DDD also makes it particularly well-suited for AI-assisted development, where boilerplate can be generated while keeping business logic centralized and reviewable.The combination of Quarkus’s developer-friendly features (like Dev Mode, CDI, and Panache) with DDD patterns creates a powerful foundation for building maintainable, scalable applications.Watch the full episode on the Quarkus YouTube channel and explore more at quarkus.io.