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