DDD Tactical Patterns in Spring Boot
Fast recall
Quick revision
Core answer
Practical Spring + DDD without polluting the domain with framework annotations; how each tactical pattern maps to a Spring construct.
Annotating domain entities with `@Entity`/`@Service`/`@Transactional`; using `JpaRepository` directly as the domain repository.
Discuss separation of domain entity from JPA entity, Spring Modulith for module enforcement, and ArchUnit rules to keep the boundary.
Interview-ready guide
Interview answer
Project structure per BC
Package per Bounded Context: com.example.sales.{domain,application,adapter,config}. domain = entities, value objects, domain services, events. application = use cases, ports. adapter = web (controllers), persistence (JPA + mappers), external clients. config = @Configuration wiring.
Multiple BCs as sibling packages — sales, shipping, support.
Boundary enforcement: ArchUnit rules in CI prevent sales from importing shipping.domain.*. Spring Modulith provides this out of the box.
Value objects with records
Money(BigDecimal amount, Currency currency) record. Compact constructor validates non-null + decimal places. plus(), zero() methods on the type. No Spring, no JPA. Records give immutability + equals/hashCode/toString for free.
OrderId(String value) record. next() generates UUID. Strongly typed IDs prevent passing CustomerId where OrderId expected.
Aggregate root with invariants and events
Order class. OrderId, CustomerId, items, status, total, internal events list. Constructor validates 'order needs items'. create() factory raises OrderPlacedEvent. cancel() validates status, mutates, raises OrderCancelledEvent. drainEvents() returns + clears.
Zero Spring imports, zero JPA. Pure domain code, unit-testable in milliseconds.
Repository port + JPA adapter
Domain layer: OrderRepository interface with save(Order), findById(OrderId).
Adapter layer: OrderJpaEntity (@Entity, @Version, @Embedded MoneyEmbeddable, @ElementCollection items). MoneyEmbeddable. OrderSpringRepo extends JpaRepository<OrderJpaEntity, String>. JpaOrderRepository implements OrderRepository, uses OrderMapper to convert domain Order ↔ OrderJpaEntity.
Domain Order and OrderJpaEntity are separate types. @Version on JPA entity gives optimistic locking at aggregate-root level. Use case sees only OrderRepository.
Application service + transactions
PlaceOrderUseCase interface. PlaceOrderCommand record.
PlaceOrderInteractor implements it. Constructor injection of OrderRepository, InventoryGateway, ApplicationEventPublisher. @Transactional on execute(): reserve inventory, create Order, save, drainEvents and publish.
@Configuration wires it via @Bean. @Transactional on use case implementation — natural transaction boundary.
Domain events with Spring
@TransactionalEventListener(phase = AFTER_COMMIT) for in-process listeners. Ensures listener runs only after producer's transaction committed — no phantom events on rollback, no stale reads.
For cross-service: transactional outbox. In same transaction, save aggregate + insert event rows into outbox table. Separate worker (or Debezium CDC) publishes from outbox to Kafka.
Outbox schema: id, aggregate_id, event_type, payload (JSONB), occurred_at, published_at.
Domain service in Spring
FundsTransferService is plain Java — transfer(from, to, amount) checks canDebit, calls debit + credit. Wired by @Configuration as @Bean. Use case loads accounts, hands to domain service, saves.
Common mistakes
- @Entity on domain entity — couples to JPA lazy proxies, dirty checking, lifecycle hooks
- @Service on aggregate or value object — they're not beans
- JpaRepository as domain repository — wrap in adapter implementing domain port
- @Transactional on controller — wrong layer
- No optimistic lock — @Version is free; prevents last-write-wins corruption
- Cross-BC imports — use ArchUnit / Spring Modulith
- Publishing events inside the transaction — use AFTER_COMMIT or outbox
- Skipping mapper — same class as domain entity AND @Entity annotated 'to save time'; short-term win, long-term coupling
Recommendation
Implement DDD tactical patterns in Spring Boot by keeping the domain layer pure Java — records for value objects, plain classes for aggregates, no framework annotations. Map domain entities to separate JPA entities in the persistence adapter. Define repositories as ports owned by the domain; implement them with Spring Data wrapped in an adapter. Put @Transactional on use case implementations, use @TransactionalEventListener(AFTER_COMMIT) for in-process events, and the transactional outbox for cross-service. Enforce per-BC boundaries with ArchUnit or Spring Modulith. Spring lives in adapters and configuration, never in the domain.
DDD Tactical Patterns in Spring Boot
Direct Answer
In Spring Boot, implement DDD tactical patterns by packaging per Bounded Context, keeping the domain layer free of Spring/JPA annotations, putting invariants on entities and value objects, defining repositories as ports in the domain, and writing use cases as plain classes wired by @Configuration. Use records for value objects, @Version on aggregate roots for optimistic concurrency, @TransactionalEventListener(AFTER_COMMIT) for in-process domain events, and the transactional outbox for cross-service events. Keep JPA entities as adapter-layer mapping artifacts — separate from domain entities. Spring lives in adapters and configuration, never in the domain.
Common Mistakes & Pitfalls
Annotating domain entities with @Entity/@Service/@Transactional; using JpaRepository directly as the domain repository.
Check your understanding
Answer these without looking back at the article.
- Why is separating domain entities from JPA entities worth the mapping cost?
- Walk through implementing the transactional outbox pattern in Spring Boot.
- How does Spring Modulith help enforce Bounded Context boundaries?
- How do you write fast unit tests for a use case without booting Spring?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.