0% прочитано

What I Would Do Differently If I Started a DDD Project Again

If I started a DDD project again, I would spend less time trying to design everything up front. I would identify invariants earlier, introduce abstractions more carefully, separate reads sooner, and let database tables influence the domain model less.

18 жовтня 2026 р. 12 хв читанняLaravel

When I started working with DDD more seriously, I wanted to get everything "right" from the beginning.

Find every bounded context, choose the right abstractions immediately, split the code into layers, and define repositories, entities, value objects, and services.

Now I see it differently. The most useful decisions appeared not when I tried to predict the entire system, but when a concrete rule became clear enough.

If I started a DDD project again, I would not try to make it "maximally DDD" from day one. I would identify much earlier which parts actually need modeling.

My main lesson is that DDD grows better together with domain understanding than from an architecture designed entirely in advance.

I Would Not Try to Define Every Bounded Context Up Front

At the beginning, a system looks clearer than it really is. Names are similar, tables are related, and use cases are still small, so it is easy to draw a boundary too early.

In the article about domain boundaries in a growing system I wrote that a boundary becomes visible through different meanings, lifecycles, reasons to change, and invariants.

Today I would treat initial bounded contexts as working hypotheses rather than a final map of the system.

Boundaries become clearer with understanding
Early assumption: Product  + registration  + access  + billing Later understanding: ProductRegistrationAccessBilling Some concepts share data.They do not necessarily share one model.

I Would Identify Invariants Earlier

I used to start from the use case: what the user clicks, which endpoint runs, and which tables need to change.

Now I would write down a simpler question much earlier: what must not become false after this operation finishes?

The answer often reveals the aggregate, transaction boundary, rule owner, and required checks.

An invariant reveals more than a list of tables
Use case: Activate product  Questions I ask now: What must be true before activation? What must still be true after activation? Which state must change together? Which failure must roll everything back?

I Would Find the Business Rule Owner Earlier

In the article about where a business rule should live I explored the difference between controller, service, and domain model.

If I started again, I would move rules to the concepts that naturally own them sooner instead of allowing them to remain in generic Services for too long.

This matters especially for rules used from HTTP, queues, commands, or cron. If a rule depends on the controller, it is probably living in the wrong place.

I Would Not Create Abstractions Without a Real Problem

DDD can easily encourage a large number of "correct" classes: interfaces, repositories, factories, mappers, and services.

Some of them are genuinely useful. But if an abstraction does not separate a concrete changing detail, express a domain concept, or simplify a dependency, it becomes ceremony.

Today I would introduce an abstraction only after I can clearly state which problem it solves.

That is also why I no longer create a repository for every model. In the article about when a Repository actually makes sense in Laravel DDD I described Repository as an aggregate persistence boundary rather than a mandatory wrapper around Eloquent.

I Would Separate the Read Side Much Earlier

One thing I kept together longer than I should have was the write model and the read model.

When an admin table starts loading aggregates, adding relations only for columns, and building complicated eager loading, that is already a useful signal.

In the article about Read Models in DDD I wrote that the read side can have its own data shape without violating DDD.

If I started again, I would allow a dedicated query object or Read Model much earlier.

Different problems can use different models
Write side: Domain model- invariants- behavior- state transitions  Read side: Query model- filters- sorting- counts- projections

I Would Let Database Tables Dictate the Domain Model Less

Laravel naturally encourages table-oriented thinking: model, migration, relation, foreign key.

For CRUD that is often excellent. But in DDD, a table is not always an entity, and a foreign key does not define an aggregate boundary.

If I started again, I would describe behavior in domain language first and decide how to persist it second.

I Would Separate Domain Language from Database Language Earlier

A column name may be historical, technical, or simply awkward. That does not mean the same name must spread through the entire domain model.

When the code starts speaking the language of the domain, incorrect boundaries and hidden rules become easier to notice.

Today I would allow a mapper or repository to translate persistence terminology into domain terminology earlier when the distinction already matters.

I Would Not Make Every Relationship Bidirectional

When a model starts from an ORM, it is easy to add relationships in both directions simply because the ORM supports it.

But the domain model does not always need to know every neighboring entity.

Fewer relationships often make an aggregate simpler and dependencies more explicit.

I Would Write Domain Tests Without HTTP or a Database Earlier

This test verifies the rule rather than the transport
public function test_product_cannot_activate_when_incomplete(): void{    $product = Product::draft(        code: null,        name: 'Example',    );     $this->expectException(        DomainException::class,    );     $product->activate();}

When a rule is easy to test without framework layers, it is a good sign that it truly lives in the model.

These tests also reveal faster whether we understand the invariant or are merely repeating controller behavior.

I Would Be Less Afraid to Change the Model When Understanding Improves

I used to see changing a bounded context, aggregate, or important term as evidence that the initial model was "wrong."

Now I see it differently. If the team understands the domain better, the model should be allowed to become more precise.

The worse outcome is not changing the model. It is keeping an old model only because a lot of structure has already been built around it.

In Center, I Would Start from Rules Rather Than an Architecture Map

While working on Center, I gradually moved toward exactly this approach.

The most useful moments did not come from abstractions created in advance, but from being able to state a rule clearly: what is valid state, who owns it, what must change together, and what is only a reaction after the fact.

If I started that journey again, I would make those questions central much earlier.

I intentionally do not bring the internal Center map into this article. The lessons matter more here than the specific modules.

My New Order of Work Would Be Simpler

Less architecture up front, more focus on rules
1. Describe the business action. 2. Write down the invariant. 3. Find the rule owner. 4. Define the smallest consistency boundary. 5. Keep the first model simple. 6. Separate reads when read needs diverge. 7. Add abstractions only for a real boundary. 8. Refine bounded contexts as language becomes clearer.

What I Definitely Would Not Do Again

I would stop doing several things automatically:

  • I would not try to draw every bounded context before enough real use cases exist;
  • I would not create repositories, interfaces, or services only because DDD is expected to have them;
  • I would not let controllers own domain rules for too long;
  • I would not load aggregates for every list and table;
  • I would not treat a foreign key as proof of one aggregate;
  • I would not be afraid to change an early model after learning more about the domain.

DDD Became a Way to Ask Better Questions

If I started a DDD project again, I would spend less time asking "which pattern should I use here?" and more time asking "which rule are we protecting?"

I would rely less on database tables as the ready-made model. I would separate reads earlier. I would not introduce abstractions before a real boundary appears. And I would allow bounded contexts to evolve with our understanding.

For me, a mature DDD approach no longer means more classes. It means understanding more precisely where a rule belongs, what must remain consistent, and which complexity is actually worth modeling.

If I started DDD again, I would design less up front and identify rules earlier. Architecture should grow from domain understanding, not the other way around.