Menu
Module 8 / 8 Advanced 18 min read

Domain Services, Events & the Whole Picture

What to do with logic that doesn't belong to any one Entity, how to name a fact that already happened, and how all eight modules fit into one coherent model.

In this module

Cover Domain Services, Domain Events, Factories, and Modules, separate the domain layer from the surrounding architecture, and assemble the bookstore's full model end to end.

Domain Service: logic that doesn’t belong to one Entity

Not every rule fits naturally on a single Entity or Value Object. A Domain Service holds domain logic that doesn’t naturally belong to any one of them — typically because the operation genuinely involves several domain objects, or represents a significant business process in its own right. It’s stateless, and it’s named after an activity in the Ubiquitous Language — a verb — not a thing.

The bookstore’s allocation rule from Module 3 — “prefer a single warehouse; only split a shipment when no single warehouse can fill the whole order” — is the clean example. It can’t live on Order alone, because deciding it requires checking inventory levels across multiple warehouses, which Order has no business knowing about directly:

final class FulfillmentAllocationService {
    List<WarehouseAllocation> allocate(Order order, List<WarehouseStock> stock) {
        var singleWarehouse = stock.stream()
            .filter(w -> w.canFulfillEntirely(order))
            .findFirst();
        return singleWarehouse
            .map(w -> List.of(new WarehouseAllocation(w.id(), order.lines())))
            .orElseGet(() -> splitAcrossWarehouses(order, stock));
    }
}

Code signals: a stateless class — no mutable instance fields — whose methods take domain objects and return a domain result; logic that coordinates two or more Aggregates to produce one outcome.

Note. Be skeptical of the Service suffix. Most classes named ...Service in a typical Spring codebase are Application Services — orchestration code that manages transactions, security, and calls into repositories, but contains no business rules of its own. An Application Service calling orderRepository.save(order) inside a @Transactional method is plumbing. A Domain Service is classified by content — does it actually contain a business rule? — never by the suffix on its name.

Domain Event: a fact, stated in the past tense

A Domain Event captures something meaningful that already happened in the domain — a fact domain experts care about, always named in the past tense: OrderPlaced, PaymentReceived, ShipmentDispatched. Events are how Bounded Contexts that shouldn’t directly depend on each other’s internals still coordinate — recall from Module 5 that OrderPlaced is Sales’s Published Language: Billing and Shipping both react to it without either depending on Sales’s internal model.

record OrderPlaced(OrderId orderId, CustomerId customerId, Money total, Instant occurredAt) {}

Characteristics: named in the past tense; immutable, since it records something that already, unchangeably, occurred; carries the data needed to act on it, almost always including the id of the Aggregate that raised it; published and subscribed to through some kind of event mechanism — an application event publisher, a message broker, or an outbox table written in the same transaction as the Aggregate change.

Code signals: immutable types, often records, named in the past tense; types pushed through a publish/dispatch call (publisher.publish(event)); listener methods (@EventListener, a message consumer) on the receiving side — these are your best tool for tracing how facts actually flow between the contexts you mapped in Module 5.

Factory: guaranteeing a valid birth

A Factory encapsulates the creation of a complex Aggregate or Value Object so it’s never possible to end up holding one that violates its own invariants. You’ve already seen one: Order.place(customerId, lines) from Module 7 is a static factory method, not a public constructor, precisely so “an order must have at least one line” can be enforced at the single moment the object comes into existence, rather than hoped for afterward. A Factory becomes a dedicated class instead of just a static method once construction needs enough external context — pricing rules, inventory checks — that cramming it into the Aggregate itself would obscure the model rather than clarify it.

Code signals: static factory methods (Order.place(...), Money.of(...)); dedicated *Factory classes or interfaces; builders that assemble a complex object and validate it before ever handing back a reference.

Module: a package named like the business, not the architecture

A Module, in DDD terms, is just a named package — but the naming convention matters more than it sounds like it should. com.bookstore.sales.order groups concepts by what they mean; com.bookstore.dao or com.bookstore.service groups them by what technical layer they happen to sit in. The first tells a reader about the domain. The second tells them about the architecture and says nothing about the business at all. Prefer domain-named packages, and treat a codebase organized entirely by technical layer as a sign that nobody has drawn the Bounded Context boundaries from Module 4 yet — the two usually go together.

The architecture around the model

Tactical patterns sit inside a layered (or hexagonal / ports-and-adapters) architecture, and it’s worth separating what you’ve actually been modeling from the plumbing around it:

  • Domain layer — Entities, Value Objects, Aggregates, Domain Services, Domain Events, and Repository interfaces. This is everything covered in this course. This is the model.
  • Application layer — the Application Services mentioned above: orchestration, transactions, security, calling into repositories. Useful for seeing how Aggregates get used in practice, but its classes aren’t part of the model itself.
  • Infrastructure layer — Repository implementations, ORM mappings, messaging, HTTP controllers, external API clients. Mostly noise as far as the model goes — but this is exactly where the Anticorruption Layers and adapters that inform your Context Map (Module 5) actually live.

When you open an unfamiliar class, asking “which of these three layers is this?” before anything else will save you from mistakenly modeling an orchestration detail as if it were a business rule.

Assembling the bookstore’s model, end to end

Putting all eight modules together: the domain is bookstore order fulfillment (Module 1). It splits into the core Sales subdomain and the supporting/generic Billing and Shipping subdomains (Module 3), each realized as a Bounded Context with its own version of Customer (Module 4), related by a Context MapSales as Customer–Supplier to Billing, OrderPlaced as a Published Language both Billing and Shipping consume, an Anticorruption Layer at Shipping’s edge against the courier API (Module 5). Inside Sales, Order is an Entity and Aggregate Root, OrderLine and Money are Value Objects living inside its boundary, OrderRepository is the one Repository for that root (Modules 6–7), FulfillmentAllocationService is a Domain Service spanning Order and warehouse stock, OrderPlaced is the Domain Event that hands facts to the other contexts, and Order.place(...) is the Factory method guaranteeing every Order is born valid (Module 8). Every name in that paragraph is also a row in the Sales glossary from Module 2 — that correspondence, end to end, is the Ubiquitous Language.

Closing principles

A handful of habits carry this discipline past the bookstore and into whatever codebase you read next:

  • Most code wasn’t built with DDD in mind, and that’s the normal case, not an exception. A plain class with id-based equality and real behavior is an Entity whether or not anyone on the original team had heard of Evans.
  • Classify by responsibility, never by suffix or annotation. Service, Manager, @Entity are hints. What the type actually does is the verdict.
  • Aggregate boundaries are inferred, never declared outright. State your reasoning, expect to revise it, and treat that revision as the discipline working correctly.
  • Attach evidence to every claim. A model nobody can trace back to actual code or actual conversation is just a diagram again — exactly what Module 1 warned against.
  • Report tensions honestly. An OrderLineRepository that shouldn’t exist, a context with no consistent language, a legacy system that’s a Big Ball of Mud — these are findings, not failures to hide.

You’ve now got the full vocabulary: Ubiquitous Language, Subdomain, Bounded Context, Context Map, Entity, Value Object, Aggregate, Repository, Domain Service, Domain Event, Factory, Module. The Glossary collects all of them in one quick-reference table — including the Java signal that gives each one away fastest.