Where is my domain?
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.
- Each invoice had to be validated against a specific legal entity in the country where it was issued.
- Every country has its own data requirements, formats, and workflows.
- We needed a clean, generic validation API, but we also needed country-specific implementations flexible enough to match each provider’s constraints.
- Cabify itself could not validate an invoice, since that requires becoming a specific type of legal entity. Third-party external providers performed the checks.
Simplified model of the problem
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.
Modeling the problem
We started by listing what we actually controlled.
- We did not control tax law.
- We did not control the external providers’ rules or their release cycles.
But we did control:
- How and when we called them
- How we mapped invoice data into their formats
- Which legal entity to use for a given invoice
- What to do with partial failures
- How to expose the result back to the rest of the platform
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.
Modeling the solution
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.
Validation attempt state transitions
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 to the rescue
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.
Hexagonal architecture models the solution
Own the gap
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.
Victor Moreno
Software Engineer
Bringing our culture to life through stories—discover it in our blog.
Scaling Chatbot Intelligence: Our Path to Multi-Agent AI
From fragmented workflows to a Connected experience
After the Prompt: How Anyone at Cabify Became a Builder
Stacking
Python Concurrency: Serving ML Without Dying in the Process
Scaling inclusive design: accessibility as a base
The CREP Experience: Building at scale as a fresh graduate