Putting It All Together in Domain-Driven Design
Domain Services, Factories, and the Complete Model
Logic that checks conditions across several objects at once ends up in a service method because none of those objects owns it. Creation logic that outgrows a constructor, needing to derive values from multiple inputs, ends up there too. The service becomes the default destination for domain logic that found no better home.
Domain services give inter-object logic a proper home. Factories give complex creation a dedicated place. Both are patterns from domain-driven design that fill the last gaps in the tactical toolkit, and filling them is also the moment the full picture comes into view.
The previous articles built that toolkit piece by piece, using an online learning platform as the running example. Entities and value objects, aggregates, domain events, repositories, and application services each arrived to solve a specific problem. This article introduces the two remaining patterns through the problems that reveal them, then steps back to show the complete model working as a coherent whole.
A Use Case Across Two Contexts
On the learning platform, a student completes a course and a certificate should be issued. Those two things belong to different parts of the system, and the separation is deliberate.
The learning context owns enrollment and course progress. It tracks which lessons a student has completed, whether assessments have been passed, and when a course reaches completion. The certification context owns certificates: what they contain, who receives one, and how they are issued.
The two contexts have separate models. In the learning context, a course is a curriculum with lessons, assessments, and a publication state. In the certification context, a course is a named subject on a document. The same word, two different models, each internally consistent.
When all course requirements are satisfied, the enrollment aggregate raises a CourseCompleted domain event. That event carries the student and course identifiers, just enough for the receiving context to do its work. It does not carry the enrollment model or the full course structure. The certification context does not need them and should not depend on them.
This is the integration pattern at work. The learning context announces what happened, and the certification context reacts without either side knowing the internals of the other.
But a question arises in the learning context before the event is ever raised: is the course actually complete? Lesson progress is tracked on the enrollment aggregate. Assessment results live separately. The course itself defines what is required. No single aggregate owns all three.
The CourseCompleted event crosses the boundary only once that question is answered. An application service in the certification context handles it and begins coordinating. One question surfaces that it cannot answer alone. How does the certificate get created? A certificate requires a recipient name, a course title, an issuance date, and an issuing authority. These must be derived from existing data and assembled before anything is persisted. That is more than a coordinator is meant to handle.
Both gaps are real, and each belongs to the context where the data lives.
Logic With No Natural Owner
The completion check draws on lesson progress from the enrollment aggregate, assessment results from within the same context, and requirements from the course aggregate. No single aggregate owns all three, and an aggregate cannot load others to answer a question that spans them.
Placing the check in the application service would solve the immediate problem but create a worse one. Whether a student has met the course requirements is a domain rule. It belongs in the domain layer, where it can be expressed in the language of the domain and tested against plain domain objects, without any infrastructure. A rule that lives in the application service is a rule that has drifted out of the domain model.
A domain service is the right home for logic that belongs to the domain but has no natural aggregate owner.1 It is stateless, operates on domain objects, and takes its name from the ubiquitous language. On the learning platform, something like CourseCompletionPolicy reflects the domain language. It names the concept and how the check is framed, not the pattern. Following the pattern name instead would produce CourseCompletionService, which names the architectural role rather than the domain concept.
The application service in the learning context calls it after each lesson completion or assessment submission. The domain service receives the relevant aggregates, applies the completion rules, and returns whether all requirements are satisfied. When they are, the enrollment aggregate records the fact and raises CourseCompleted. The rule stays in the domain layer. The application service stays focused on coordination.
With completion confirmed and the event raised, the certification context receives the trigger. The remaining problem is creating the certificate itself.
Creation That Belongs in the Domain
Creating a certificate requires assembling properties from several domain objects. The recipient name comes from the student record. The course title comes from the certification context’s own course record. The issuance date is derived at the moment of creation. The issuing authority depends on the kind of certificate.
Without a factory, the application service has to know which field on the student record is the recipient name and which field on the course record is the title. That is domain knowledge accumulating in the coordinator.
Putting that logic inside a constructor is no better. The constructor accepts domain objects, derives the certificate’s properties from them, and then initializes itself. That looks like construction but behaves like assembly.
A factory makes the intent explicit. It receives the student and the course, and takes full responsibility for assembling a certificate from them. The application service passes domain objects and gets back a certificate. If the rules for what appears on a certificate change, the factory changes. The application service does not.
A factory does not have to be a separate class. A static factory method on the Certificate aggregate, such as Certificate.issue(student, course), keeps the creation logic in the domain without introducing a dedicated object. The pattern describes where creation responsibility belongs, not how it must be structured.
The application service calls the factory, receives the certificate, persists it through the repository, and the use case is complete.
The Full Picture
The sequence from course completion to issued certificate passes through every layer of the design.
In the learning context, the completion service enforces the rule that spans multiple aggregates, and the enrollment aggregate raises CourseCompleted once that rule is satisfied. The event carries the student and course identifiers and nothing more. No enrollment model, no lesson list, no assessment record travels with it. The boundary holds. The certification context receives a fact about what happened, not a window into the learning context’s internals.
In the certification context, the application service handles the event and loads what it needs through its own repositories. The student record and course record it works with are the certification context’s own models, shaped by what issuing a certificate requires. They are not the same objects that exist in the learning context. Each context has its own representation of the same real-world concept.
In the certification context, the factory assembles the certificate from the available domain objects. The repository persists it. Any events the new aggregate records are dispatched to their handlers.
The strategic layer is visible at the boundary. Two contexts with separate models, connected by an event that carries only identifiers. The learning context does not know what happens after the event is raised. The certification context does not know what caused the event to appear. Each can change its internal model without affecting the other.
The tactical layer is visible on both sides of the boundary. In the learning context, the domain service applied logic that no single aggregate owned, and the enrollment aggregate recorded the result as an event. In the certification context, the application service coordinated, the repository loaded what was needed, and the factory kept the creation knowledge in the domain. Each pattern in its place, each doing one thing.
What Each Pattern Prevents
The first article in this series described software that drifts from the business it models. Features added in the wrong place. The same concept named differently in code than in conversation. Rules scattered across service methods and utility classes. Every pattern introduced since is a specific defence against one form of that drift.
Ubiquitous language closes the gap between how the business talks and what the code says. Without it, the model decouples from the domain as vocabulary diverges and the code begins to reflect the developer’s interpretation rather than the business reality.
Bounded contexts prevent a large domain from being forced into a single model where every concept must serve every context at once. Without them, a concept like “course” accumulates fields and rules it needs in some contexts and not others, until no team can change it safely. Context mapping makes the relationships between contexts explicit, so that assumptions about what one context expects from another are visible rather than discovered when something breaks.
Integration patterns address coupling at the boundary. The choice between a direct call and a domain event turns on whether the downstream reaction is a prerequisite or a consequence. Getting that choice right keeps contexts from accumulating hidden dependencies on each other’s timing and availability.
Matching the architecture to the subdomain type ensures that the core domain gets the isolation it needs, while supporting and generic subdomains are not burdened with complexity they do not require.
Entities and value objects give domain concepts proper representation. Without them, rules scatter across every caller that handles a raw string or number, and the same validation gets duplicated until some copy is out of date. Aggregates enforce consistency boundaries so that business rules have a single owner and cannot be bypassed by code that reaches past the root to modify its parts directly.
Domain events record what happened as facts rather than triggering synchronous reactions. Without them, a state change in one part of the system binds every consequence to the same call stack, and the logic for what happens next mixes into the logic for what caused it.
Repositories keep persistence details out of the domain so that infrastructure choices do not follow domain objects into tests that should only care about business rules. Application services keep coordination out of the domain model, so the code that loads aggregates, manages transactions, and dispatches events does not mix with the rules the domain is supposed to enforce.
Domain services keep inter-aggregate logic in the domain layer, where it is visible to the model and testable without infrastructure. Factories keep creation logic from drifting into constructors that become coordination workflows, or into application services that absorb responsibilities they were never meant to own.
Closing the Circle
The first article in this series opened with a simple observation. Software has a tendency to drift from the business it is supposed to model.
That drift is not a single event. It accumulates in small decisions: a rule added to the wrong layer, a concept named differently in code than in conversation, a boundary left implicit until it hardens into a constraint. Each compromise is small. The distance grows over time.
The patterns in this series are the systematic answer to that. Not a single solution, but a vocabulary. One pattern for each form of drift. Together they describe how to keep a codebase aligned with the business it represents, from the boundaries that divide a large domain into coherent parts, through the integration decisions that connect those parts without coupling them, to the objects and services that carry the rules in the right place.
The code that results is not finished and it is not perfect. But it reflects the business it models. That is what domain-driven design set out to do.
Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003. ↩︎
This article is part of the Domain-Driven Design series
- 1. What Is Domain-Driven Design?
- 2. Subdomains and Bounded Contexts in Domain-Driven Design
- 3. Context Mapping in Domain-Driven Design
- 4. Integrating Bounded Contexts
- 5. Matching Architecture to the Subdomain
- 6. Entities and Value Objects in Domain-Driven Design
- 7. Aggregates in Domain-Driven Design
- 8. Domain Events in Domain-Driven Design
- 9. Repositories and Application Services in Domain-Driven Design
- 10. Putting It All Together in Domain-Driven Design