Engineering

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 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 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 boundaries 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

Choose which cookies
you allow us to use

Cookies are small text files stored in your browser. They help us provide a better experience for you.

For example, they help us understand how you navigate our site and interact with it. But disabling essential cookies might affect how it works.

In each section below, we explain what each type of cookie does so you can decide what stays and what goes. Click through to learn more and adjust your preferences.

When you click “Save preferences”, your cookie selection will be stored. If you don’t choose anything, clicking this button will count as rejecting all cookies except the essential ones. Click here for more info.

Save preferences