A practical series about Domain-Driven Design in Laravel: domain boundaries, aggregates, business rules, services, transactions, events, and read models from a real project.
CRUD works well while an entity is truly simple. Once states, allowed transitions, dependencies, and rules appear, a controller starts knowing too much. Simple examples show when a domain model becomes more useful than a collection of create, update, and delete operations.
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.
A foreign key does not mean two entities must change as one unit. In practice, an aggregate is defined by the rules that must stay consistent in one change, not by the database schema.
A business rule is easy to leave in a controller or application service at first. Once several use cases depend on it, the rule often reveals itself as a domain invariant. This article shows how I separate request validation, use-case coordination, and domain rules.
For a long time it was easy for me to call any non-trivial logic a service. In practice, one distinction helped: an application service coordinates the use case, transaction, repository, and external dependencies, while a domain service expresses a domain rule that does not naturally belong to one object.
One business operation does not always require one large transaction. In practice, I separate what must change consistently inside one aggregate from secondary work that can happen after commit.
Using ProductRegistry, I show several practical DDD decisions: how Domain, Application, Infrastructure, and Delivery are separated, why ProductRegistration and BootstrapInvitation have their own boundaries, where events fit, and where one strong-consistency transaction is the better model.
I do not use domain events everywhere. If the second action is required to protect an invariant, I keep it as a direct call in one transaction. If another part of the system only reacts to an already committed fact, a domain event is often a better fit.
The domain model primarily protects rules when state changes. A list, table, or API response often needs a different data shape. This article explains why a separate read model does not violate DDD and often makes the code simpler.
A string is enough while the value has no rules of its own. Once normalization, validation, comparison, or domain meaning appears, a Value Object can make those rules explicit instead of scattering them across services.
Domain, Application, and Infrastructure folders can help organize code, but they do not create DDD by themselves. If business rules still live in controllers and generic services, only the directory structure has changed.
A Repository is not needed for every Eloquent model. In DDD, it makes sense as an aggregate persistence boundary or when the application layer should not know persistence details.
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.