Spring, DDD & Architecture Workshop Notes
Spring Security: Upcoming Fixes (May 18)
Rollout date: 18. Mai
Spring releases a batch of security fixes discovered via the Mythos vulnerability check. All branches up to Spring 2.7 are affected.
- Blog post with full details drops next week
- If you run anything ≤ 2.7, plan the upgrade now
- Full talk at DevOps Antwerpen covers this in depth
Domain-Driven Design (DDD)
Extensional vs. Intensional
| Term | Meaning | Examples |
|---|---|---|
| Extensional | Represents external reality — maps things from outside the domain | DTOs, API models, database rows |
| Intensional | Expresses domain concepts from within — gives things meaning | Value Objects, Entities, Aggregates |
The goal: code that reads like the domain language, not like database schema.
Building Blocks
Entity — has a unique identity that persists over time.
// Identity matters — two Orders with same items are still different Orders
@Entity
public class Order { private OrderId id; ... }
Value Object — defined by its attributes, not identity. Immutable.
// Two Money(10, EUR) instances are equal — no identity needed
record Money(BigDecimal amount, Currency currency) {}
Aggregate — consistency boundary. One Aggregate Root controls all internal state.
- Only the root is accessible from outside
- Repositories manage Aggregates (not raw Entities)
- JPA manages Entities inside an Aggregate internally
// Root controls invariants for the whole cluster
@AggregateRoot
public class Order implements AggregateRoot<Order, OrderId> {
private List<LineItem> items; // LineItem = Entity inside the Aggregate
}
Key distinction: Aggregates are fetched/saved via Repositories. Sub-Entities live only inside an Aggregate and are accessed through it.
Fairbanks (Architecture Fitness Functions)
“Fairbanks” refers to fitness functions from Just Enough Software Architecture (George Fairbanks) — automated checks that verify architectural rules continuously. Think: ArchUnit tests that fail the build if a module boundary is violated.
jMolecules
GitHub: https://github.com/odrotbohm/tactical-ddd-workshop
jMolecules bridges DDD concepts and Java code. Three layers of value:
1. Expressive Annotations & Interfaces
// Marker interfaces provide generic type safety
public class Order implements AggregateRoot<Order, OrderId> { }
public class OrderId implements Identifier<Order> { }
Annotations like @Aggregate, @Entity, @ValueObject make intent explicit — not just for humans, but for tooling.
2. ArchUnit Verification
jMolecules ships ready-made ArchUnit rulesets. Add to your test suite:
@AnalyzeClasses(packagesOf = Application.class)
class DddRulesTest {
@ArchTest
ArchRule dddRules = JMoleculesDddRules.all();
}
This enforces: Aggregates only access other Aggregates via Associations, Repositories only handle Aggregate Roots, etc.
3. ByteBuddy Plugin — Boilerplate Elimination
The problem: JPA requires a ton of annotations that have nothing to do with the domain:
// JPA-heavy noise
@javax.persistence.Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue private Long id;
@OneToMany(cascade = CascadeType.ALL) private List<LineItem> items;
}
With jMolecules + ByteBuddy plugin: The Maven/Gradle plugin reads your DDD annotations at compile time and generates the JPA mappings automatically.
// Clean domain model — JPA annotations added at build time
@Aggregate
public class Order {
private OrderId id;
private List<LineItem> items;
}
This makes the code framework-independent — swap JPA for something else later without touching domain classes.
Logical Structure View & Stereotype Annotations
Using @Aggregate, @DomainService, @Repository as stereotype annotations enables tooling to show the abstraction layer of the code, not just the folder structure.
Why stereotypes beat packages for structure:
- Packages enforce rigid nesting; stereotypes compose
- Multiple concerns can be expressed on one class
- Tools (planned IntelliJ port) can render a “DDD view” of the codebase independent of file layout
AI angle: New stereotype-based patterns are more machine-readable. LLMs can reason about @Aggregate boundaries more reliably than inferring structure from arbitrary package names.
Spring Modulith
Repo: https://github.com/odrotbohm/arch-evident-spring
Blog post: https://odrotbohm.de/2025/12/rethinking-spring-application-integration-testing/
Core Idea: Sliced Onion Architecture
Traditional layered architecture splits code horizontally (web → service → repository). This creates tight coupling across features.
Spring Modulith splits vertically — by business capability:
com.example
├── order/ ← module: public API + internal impl
│ ├── OrderController.java (public)
│ ├── OrderService.java (public)
│ └── internal/
│ └── OrderRepository.java (package-private = module-internal)
├── inventory/ ← module
└── customer/ ← module
Rule: Only public types in a module package are accessible to other modules. Package-private = module boundary enforced at test/build time.
Module Integration
Three levels, from tight to loose coupling:
| Approach | Coupling | When to use |
|---|---|---|
| Direct bean injection | High | Avoid across modules |
| Synchronous domain events | Medium | Same transaction needed |
| Async transactional listeners | Low | Eventual consistency OK |
// Async event listener — Order module doesn't know about Inventory
@TransactionalEventListener
@Async
void on(OrderCompleted event) {
inventory.reduce(event.getItems());
}
Event Publication Registry
Problem: async listener fails after event published → event lost, data inconsistent.
Spring Modulith solves this with an Event Publication Registry — persists the event before dispatching, marks it complete only after successful handler execution. Unpublished events can be retried.
Testing with @ApplicationModuleTest
Standard Spring @SpringBootTest boots the entire application. Spring Modulith’s annotation boots only the relevant module + declared dependencies:
@ApplicationModuleTest
class OrderModuleTest {
// Only Order module beans available — Inventory is mocked/excluded
@Test
void placingOrderPublishesEvent(Scenario scenario) {
scenario.stimulate(() -> orders.place(new OrderRequest(...)))
.andWaitForEventOfType(OrderCompleted.class)
.toArrive();
}
}
Benefits vs. classic integration tests:
- Faster startup (smaller context)
- Fails if you accidentally introduce cross-module coupling
- Acts as architectural fitness function — unintended dependency = test failure
Observability
Spring Modulith exposes an Actuator endpoint with the module dependency graph as JSON — machine-readable architecture documentation that’s always up to date.
Book Recommendations
| Book | Author | Why relevant |
|---|---|---|
| Sustainable Software Architecture | Carola Lilienthal | Keeping architecture clean long-term, managing complexity |
| Domain-Driven Design | Eric Evans | The original — Aggregates, Bounded Contexts, Ubiquitous Language |
| Just Enough Software Architecture | George Fairbanks | Fitness functions, risk-driven architecture, the “Fairbanks” concept from the talk |