Mar 23, 2026
While learning Domain Driven Design (DDD), most examples are canonical. They are intentionally simple. They fit the patterns. They show the concepts. Real work is not shaped that way. In practice, problems rarely map cleanly to the textbook domains. You must evaluate value, delivery pressure, quality needs, and legacy constraints. You must apply engineering principles!
This article explores the space between theory and daily work. It uses real cases and the reasoning behind each decision.
The goal is to show how you can locate a domain that is not obvious, extract it, and model it so that it becomes useful.
Many engineers have read the Product-Line example many times. It explains aggregates, invariants, and boundaries well. Yet in day-to-day work, there is often no “product line” hiding inside your problem. Trying to force one only adds friction. The challenge is not to map your system onto a canonical example.
The challenge is to identify the real domain where the rules actually live.
A useful example comes from an invoice-validation problem handled by my team in Cabify’s Fintech division.
This created a structural gap: if the core rules live outside our system, where is the domain? The setup did not match the classic DDD template. There was no obvious aggregate, invariant, or entity to anchor a model. The domain seemed to sit outside the codebase.
We started by listing what we actually controlled.
But we did control:
Once framed that way, a domain started to appear. It was not “invoice validation” itself. It was the orchestration around validation: selecting the right legal entity, routing to the correct provider, tracking the lifecycle of a validation attempt, handling retries and fallbacks, and maintaining a consistent view of the invoice validation status inside our system.
The rules were not about VAT or withholding taxes. The rules were about trust, responsibility, and state transitions under uncertainty. That was our domain.
Once we recognized that the real complexity lived in the orchestration, we could begin to model it. The first step was to define the lifecycle of a validation attempt. An invoice could be pending, in progress, validated, rejected, or failed due to an external error. Those states were internal and independent of the provider’s vocabulary. They reflected our responsibility, not the provider’s. Then we defined the rules that governed transitions: when to trigger a retry, when to escalate a provider failure, when to freeze further processing, and when to declare the validation complete. This gave us a stable surface. Providers could change their APIs or their error catalogues, but our domain model remained steady. The invariants belonged to us. The variability stayed at the boundaries.
With that structure in place, the boundary design became straightforward. Each country-specific implementation was reduced to a small adapter whose job was to translate our canonical request into the provider’s format and to convert the provider’s response back into our internal model. The adapters did not own business rules. They only bridged protocols. This separation made the system resilient. If a provider changed an endpoint, added a field, or altered an error code, the impact was isolated. The core remained unchanged because the domain logic stayed inside the orchestration layer. What initially looked like “just calling external services” became a stable domain of its own once we identified the rules we actually owned.
We saw this clearly when we looked at concrete provider differences. Some validation providers exposed a synchronous request–response API. You sent the invoice data and got an immediate validation result. Others were fully asynchronous. You submitted a request, then later received the outcome through a webhook. Our model could not depend on either style. Instead, we defined a single internal concept of a “validation attempt” and its states. For sync providers, the attempt moved from pending to validated or rejected in one step. For async providers, it moved from pending to in progress when we sent the request, then advanced when the webhook arrived. The orchestration layer absorbed the protocol differences. The rest of the system only saw consistent state transitions.
Another example was invoice numbering. Some providers required us to maintain the sequence of invoice serial numbers. We had to track the next number for each legal entity and ensure there were no gaps or duplicates. Other providers managed numbering themselves and simply returned the assigned serial number in the response. Again, the rules we cared about were not “how to number invoices in country X.” The rules were: who is responsible for numbering, when do we lock a number, when can we reuse it, and how do we expose a stable identifier to downstream systems. For one provider, the model might be a “CorrelativeSequence” we control; for another, only a record of what the provider assigned. In both cases, numbering cannot be generalized across providers and needs to be handled in each adapter.
Hexagonal architecture provided the structure to model this domain cleanly. The core contains the orchestration logic and depends only on ports (interfaces defining validation contracts). Adapters implement these ports, translating between our domain model and each provider’s API. When a provider changes or we add a new country, only the adapter changes. The domain logic stays stable because it depends on abstractions, not implementations.
This reframing resolved the initial question. The domain was not missing. It was misplaced. It lived in the coordination, not in the validation logic itself. Once identified, it could be modeled, tested, evolved, and understood in the same way as any other domain. And the payoff was straightforward: simpler integrations, clearer boundaries, and an architecture that remained stable even as the external landscape changed.
If you face a problem that does not resemble the textbook DDD examples, do not assume that DDD does not fit your problem straight away. List what you actually control. Map the rules you enforce, not the ones imposed from outside.
Model the invariants that belong to you and the domain will emerge.
Stop looking for the domain in the textbook. Start looking for it in the gap between what the world gives you and what your system needs to guarantee. That gap is yours. Own it.
Software Engineer