DDD is often explained through terms such as Entity, Value Object, Aggregate Root, Repository, and Domain Event.
In real code, I find another set of questions more useful: where does responsibility belong, who owns state, and which rules must remain consistent after an operation completes.
In AVN Center, this is clearly visible in ProductRegistry. The module deals with product registration, bootstrap authority, identity, persistence, an external API, and scenarios where several related changes must either succeed together or not become authoritative at all.
These decisions show how I use DDD not as a collection of patterns, but as a way to make responsibilities explicit in code.
Domain, Application, Infrastructure, and Delivery Change for Different Reasons
Delivery HTTP contract request / response mapping | vApplication use case coordination persistence contracts policies | vDomain state identities invariants valid transitionsIn ProductRegistry, I do not use these layers simply as a directory structure.
Domain owns model state and rules. This is where ProductRegistration, BootstrapInvitation, identities, and valid state transitions belong.
Application coordinates a use case. For example, EstablishOrResumeProductRegistration works through owner-defined repository and policy contracts rather than directly through an Eloquent model.
Infrastructure implements those contracts: persistence, database constraints, locking, and transaction mechanics.
Delivery owns the external boundary: HTTP contracts, request mapping, headers, status codes, and responses.
This gives me a simple rule: a business decision should not depend on how HTTP or persistence happens to be implemented today.
final readonly class EstablishOrResumeProductRegistration{ public function __construct( private ProductRegistrationRepository $repository, private ProductRegistrationIdGenerator $idGenerator, private ProductRegistrationProtocolPolicy $protocolPolicy, ) {} public function execute( ProductIdentity $productIdentity, ProductInstanceIdentity $productInstanceIdentity, BootstrapIdempotencyKey $bootstrapKey, array $supportedProtocolVersions, ): ProductRegistration { $existing = $this->repository ->findByBootstrapKey($bootstrapKey);ProductRegistration and BootstrapInvitation Remain Separate Aggregate Roots
ProductRegistration and BootstrapInvitation participate in the same bootstrap flow, but that does not make them one Aggregate.
ProductRegistration owns the registration relationship and its lifecycle.
BootstrapInvitation has a different responsibility: it represents bounded bootstrap authority and has its own identity, lifetime, environment, binding, and reuse rules.
One object therefore does not become an internal part of another simply because they meet in the same Application use case.
For me, an Aggregate boundary is primarily defined by its own invariants and lifecycle, not by how close two objects appear in a flow.
public function bind( ProductRegistrationId $registrationId, DateTimeImmutable $at,): void { if ($this->boundRegistrationId !== null) { if ($this->boundRegistrationId->equals($registrationId)) { return; } throw BootstrapInvitationBindingConflict ::alreadyBoundToAnotherRegistration(); } if ($this->isRevoked()) { throw BootstrapInvitationBindingNotAllowed::revoked(); }External code should not simply assign a new bound_registration_id.
The bind() operation goes through the Aggregate Root itself, so it can enforce whether the invitation was already used, revoked, or expired.
This is one practical value of an Aggregate Root: state changes pass through the place that owns the corresponding rules.
An Event Belongs Where It Actually Separates Responsibilities
I do not introduce events automatically after every important Aggregate change.
An event is a good fit for a fact that has already happened and to which other parts of the system may react independently.
For example, after a successful authoritative operation, Audit, observability, projections, or other reactions can happen separately. A short delay does not make the original business operation incorrect.
But when the second step belongs to the same invariant, placing an event between them introduces an intermediate state that the system must be able to tolerate safely.
So the question for me is not "can I publish an event here?" but "can the second part of this work safely happen later?"
Good fit for an event: authoritative state committed | +--> Audit +--> observability +--> projection A short delay is acceptable. Different case: ProductRegistration established | +--> BootstrapInvitation binding still pendingSeparate Aggregate Roots Can Still Require Strong Consistency
The bootstrap flow contains exactly this kind of case.
The first successful establishment or resume of a ProductRegistration together with binding the corresponding BootstrapInvitation forms one important consistency boundary.
I do not want a state where the registration is already authoritative while the invitation still appears free and could be used for a conflicting registration relationship.
Eventual consistency provides no advantage here. It would only create an intermediate state that would require additional protection, recovery, or compensation.
The layer responsibilities still remain separate: Application defines that the changes are semantically atomic, while Infrastructure implements the transaction, locking, and persistence guarantees.
Application decides the semantic operation: validate BootstrapInvitation | vestablish or resume ProductRegistration | vbind BootstrapInvitation | vsuccess Infrastructure guarantees: What I Check When Defining a DDD Boundary
In practical work, I use several questions to test a boundary:
- Who owns this state? The change should go through that owner rather than arbitrary external code.
- Where does the invariant belong? If a rule belongs to one Aggregate, that Aggregate should protect it.
- Do these two objects really share one lifecycle? A shared use case does not automatically make them one Aggregate.
- Can the second reaction happen later? If yes, an event may be a good boundary.
- Is the intermediate state acceptable? If not, strong consistency deserves serious consideration.
- Is Infrastructure leaking into the model? Domain and Application should not depend on a concrete ORM or HTTP implementation.
For Me, DDD Is Primarily About Explicit Boundaries
In ProductRegistry, DDD does not mean that every class gets a Repository, every change produces an Event, or every related object belongs to one large Aggregate.
The value appears when boundaries remain as narrow as the real rules allow.
ProductRegistration and BootstrapInvitation can remain separate Aggregate Roots while still participating in one strong-consistency operation. Application can coordinate a use case without knowing Eloquent details. Infrastructure can own the transaction mechanics without making business decisions.
That is the practical side of DDD for me: not adding more architecture, but making responsibility explicit about where it belongs.
DDD helps me not by adding more patterns, but by making ownership of state, rules, and consistency boundaries more precise.



