Skip to content

20.3 Context Integration: Domain Events, ACLs, and Model Evolution

Boundaries between limited contexts must exchange information, but they should not share the same internal models. Integration boundaries are responsible for language translation, isolating changes, and reliably propagating facts generated by local transactions.

Domain Events and Integration Events Separated

Domain events express business facts that have already occurred within a context:

java
public record RegistrationConfirmed(
    TournamentId tournamentId,
    PlayerId playerId,
    int confirmedCount
) implements DomainEvent {}

They can trigger rules within the same context, but they are not necessarily suitable as public messages. Integration events are stable contracts intended for external consumers, and they may hide internal fields, rename properties, or add versioning metadata:

json
{
  "eventType": "TournamentEntryConfirmed",
  "schemaVersion": 1,
  "entryId": "entry-92",
  "tournamentId": "tournament-7",
  "participantId": "participant-42",
  "confirmedAt": "2026-08-03T10:15:30Z"
}

Serializing internal classes directly to the broker exposes refactoring changes as disruptive modifications across teams.

When Events Are Published

Aggregates can record domain events during the execution of business operations, but external event publication must occur after a transaction succeeds, and must be reliably preserved via an Outbox to ensure consistent event delivery:

text
Aggregate generates domain events
  → Application service saves the aggregate
  → Within the same transaction, writes integration events to Outbox
  → After commit, relay publishes the events

If a transaction rolls back, it is not safe to publish an "enrollment confirmed" event externally. If the relay re-publishes, consumers must be idempotent.

Anti-Corruption Layer Protection of Local Language

Assume the legacy billing system uses ORD, CUS_NO, and status code 7 to indicate authorized orders. The registration context should not let these concepts seep into the domain model:

java
final class LegacyBillingAdapter implements PaymentAuthorization {
    private final LegacyBillingClient client;

    public Authorization authorize(PaymentRequest request) {
        LegacyOrderResponse response = client.createOrder(toLegacyOrder(request));
        return switch (response.statusCode()) {
            case 7 -> Authorization.approved(response.orderNo());
            case 9 -> Authorization.declined(response.reasonCode());
            default -> throw new BillingUnavailable();
        };
    }
}

An ACL is not just a DTO Mapper. It also translates error, identity, time, unit, idempotency, and consistency semantics. If the upstream cannot distinguish between "denied" and "temporarily unavailable," the ACL cannot magically create reliable semantics, it can only explicitly expose uncertainty.

Shared Kernel Must Be Minimal and Commonly Governed

When two contexts share only a small amount of models or code, any change requires coordinated effort. Shared Kernel is only suitable for truly stable, jointly owned components, such as governed identity types or protocol schemas.

Putting all common-domain into a shared package effectively recreates a distributed monolith. Copying a simple value type can sometimes be cheaper than permanently sharing and evolving the code together.

Model Evolves with Cognitive Understanding

Domain models are not finalized in a single workshop. Production feedback may reveal:

  • The same term actually encompasses two distinct lifecycles;
  • A single aggregate suffers from excessive concurrency conflicts and needs to be split;
  • Rules initially believed to be eventually consistent actually require reserved states;
  • Call directions between contexts do not align with team responsibilities;
  • Support personnel need intermediate states that are absent from the model.

The evolution process can follow these steps:

  1. Describe current model failures using real-world instances and incidents;
  2. Refine language and invariants with domain experts;
  3. Adjust code boundaries and contracts;
  4. Migrate data using expand–migrate–contract;
  5. Update events, monitoring, documentation, and team responsibilities;
  6. Remove outdated language and compatibility layers.

When Full DDD Is Not Necessary

For data entry, simple CRUD operations, short-lived internal tools, or well-established, generic domains, clear layering and module boundaries may be sufficient. The investment in DDD modeling is most justified in core domains where business rules are complex, language ambiguity is high, changes are long-term, and the cost of errors is significant.

Avoid applying tactical patterns to oversimplify problems, and don't dismiss the value of deep modeling in core domains simply because peripheral functions are straightforward.

Chapter 6: Professional Linkage Review

text
Construction and Contracts
→ Requirements, architecture, testing, refactoring, and delivery
→ Design principles and object patterns
→ Layering, ports, and dependency rules
→ Modularizing monoliths and service extraction
→ Resilience, consistency, deployment, and events
→ AI systems, API contracts, and domain boundaries

The shared goal of this linkage is to contain changes, failures, and responsibilities within understandable boundaries, verified and validated through testing, contracts, and runtime feedback.

References

Built with VitePress | Software Systems Atlas