0% прочитано

How I Find Domain Boundaries While the System Is Still Growing

Domain boundaries are rarely obvious at the beginning. In practice, I look at language, rules, reasons for change, and lifecycle. If two parts of the system change for different reasons, that is a strong signal that they may not belong in the same context.

13 жовтня 2026 р. 10 хв читанняLaravel

In the previous article about the point where plain CRUD stops being enough I wrote about the point where data is no longer just data because rules and states start growing around it.

The next question is harder: where does one part of the domain end and another begin?

While working on AVN Center, I repeatedly saw an initial boundary look reasonable and then become less accurate as the system grew and revealed two different responsibilities inside it.

That is normal. I no longer expect the perfect system map to be drawn once at the beginning and never changed again.

For me, a domain boundary is not a folder or table boundary. It is a boundary inside which concepts, rules, and changes share the same meaning.

I Do Not Try to Guess Every Boundary Upfront

At first, many concerns naturally look like one thing
At the beginning: Product  |  +-- name  +-- code  +-- status  +-- settings  +-- access information  +-- registration information

When the feature set is small, grouping things together may cause no problem at all.

Everything refers to the same product, shares an identifier, and may even be stored close together.

I do not split that model merely because DDD should have many contexts.

It is often better to let the domain reveal its real rules first.

My First Signal: the Same Words Start Meaning Different Things

One term can hide two different models
"active" In one part:the product can be offered In another part:a user currently has access Same word.Different meaning.Different rule.

Language is one of the clearest boundary signals.

If the team says "active" but in one place means the state of the product itself and in another means whether a particular user may use it, these are no longer quite the same concept.

While the system is small, that distinction is easy to keep in your head. As scenarios grow, the shared word starts creating false assumptions.

At that point I ask: are we trying to make one model describe two different things?

Second Signal: Parts Change for Different Reasons

Different reasons for change often point to different boundaries
Product description changes because:- metadata changes- product lifecycle changes- product version changes Access rules change because:- policy changes- user state changes- time or entitlement changes

This is one of the strongest practical criteria for me.

If two groups of rules change because of different events, requirements, and decisions, their shared class or service gradually becomes accidental coupling.

They may use the same identifier, but that alone does not mean they belong to the same model.

Third Signal: Different Lifecycles

Different states and transitions may indicate different models
Thing A: draft  -> active  -> archived Thing B: pending  -> granted  -> expired  -> revoked

Two things can be closely related while living by different timing rules.

One may exist for years and rarely change. Another may be created and completed many times around it.

If they need different states, transitions, and completion rules, that is a strong reason not to force one entity to manage both lifecycles.

Fourth Signal: Different Invariants

Each part protects its own set of rules
Context A protects: "Every active producthas valid product data." Context B protects: "Access is granted onlywhen its own conditionsare satisfied."

A boundary becomes clearer when I stop asking "which tables are related?" and ask "which rules must always remain true?"

If two parts of the system protect different sets of invariants and changing one does not require changing the other at the same time, I lean toward separate contexts.

A Foreign Key Does Not Define the Domain Boundary

A database relationship does not answer the architecture question
products.id    ^    |access_records.product_id Database relationship:yes Same domain model:not necessarily

This was an important shift in how I think.

Earlier, database proximity could easily push me toward keeping behavior together.

Now I treat a foreign key as a technical relationship between data, not proof that all behavior must live in one domain model.

A Shared Use Case Does Not Automatically Mean a Shared Context

Sometimes one application use case touches several parts of the system.

That alone does not mean they should become one large model.

A shared use case can coordinate two clear models when each owns its own rules and responsibility.

I care more about who owns the rule than about who happens to participate in the same request.

A Simple Example: Product Description and the Right to Use It

Two related concepts can answer different questions
Product answers:"What is this product?" Access answers:"Can this subject use it now?" They are related,but answer different questions.

This example is close to experience I gained while evolving AVN Center, but intentionally simplified.

The product description answers what the product is, which properties it has, and what state the product itself is in.

The right to use it answers a different question: whether a particular subject may use that product now.

They are related. But the reason the product description changes is very different from the reason access changes.

Differences like these gradually reveal the real boundary to me.

I Do Not Split the Model at the First Sign of Difference

Splitting too early has a cost too.

Separate interfaces, data translation, additional dependencies, and coordination all appear.

If the rules are not truly independent yet, that separation can create more work than value.

I therefore wait for repeated signals: different language, different reasons for change, different lifecycles, and different invariants.

I Split When the Shared Model Starts Getting in the Way

Practical signals that the boundary has become visible
I consider a split when: - one class knows unrelated rules- one status means different things- changes for one feature break another- tests need unrelated setup- names become ambiguous- one lifecycle waits for another

The most important signal for me is not file size or method count.

It is the point where one model forces unrelated rules to know about each other without a real need.

At that point, separation is no longer architecture for its own sake. It removes unnecessary dependency and makes each side simpler.

A Boundary I Choose Today Is Not Permanent

This is probably the biggest relief I got from practicing DDD.

The architecture does not have to be perfectly correct before the first line of code.

The boundaries need to be good enough for what we know today and flexible enough to be refined when the domain teaches us more.

An evolving model does not mean the original architecture was bad. It often means the team learned more about the product.

When a Boundary Changes, I Move Rules, Not Just Files

A real split changes responsibility, not just folder structure
Before: One model  +-- product rules  +-- access rules After: Product context  +-- product rules Access context  +-- access rules Explicit relationbetween them

If I only move classes into two folders while one service still knows every rule from both sides, the boundary has not really changed.

The split becomes real when each side owns its language, rules, and states, and the relationship between them becomes explicit.

Tests Often Reveal a Bad Boundary Before a Diagram Does

There is a very practical signal: testing one rule requires preparing a large amount of state that seems unrelated to that rule.

If this keeps happening, I check whether two responsibilities have been mixed together.

A good boundary often makes tests more local: verifying one rule requires only the data owned by that part of the domain.

In AVN Center, the Boundaries Came from Repeated Rules, Not a Diagram

The AVN Center connection matters to me as experience rather than as a description of its internal architecture.

The boundaries became clearer when I saw repeated rules, different reasons for change, and different lifecycles.

A separate practical DDD example from my work on Center shows one practical step from that work, but the broader lesson is this: boundaries grow more reliably from system behavior than from names of future modules.

Questions I Ask Before Splitting a Domain

Before creating a separate boundary, I ask:

  • Do the same words mean the same thing here?
  • Do these rules change for the same reasons?
  • Do the objects truly share one lifecycle?
  • Do they protect the same set of invariants?
  • Does changing one almost always require changing the other at the same time?
  • Does the shared model simplify the code rather than merely reduce the number of classes?
  • Do tests for one side require a lot of unrelated state from the other?
  • Can I explain each side’s responsibility in one simple sentence?

Good Boundaries Become Visible Over Time

I used to want to find the correct boundary immediately.

Now I see a boundary as a working hypothesis about which rules belong together.

If language, lifecycle, and reasons for change remain shared, the boundary is working.

If they start diverging, the model is telling me that the boundary needs refinement.

For me, DDD in a growing system is not about predicting the future. It is about noticing when the domain has already revealed a new boundary.

I do not try to draw perfect boundaries upfront. I watch the language, rules, and reasons for change, and the system gradually reveals where the real boundary belongs.