EN · · 3 min read
DDD Is Not Folder Structure
Why domain-driven design begins with language and boundaries rather than Entities, Services, and Repositories folders.
Engineering EssayIntermediateEvergreen
Four architecture folders do not make a domain model. A solution can contain
Domain, Application, Infrastructure, and Api projects while its
business rules remain scattered across controllers, database constraints, and
conditionals that nobody can explain in domain language.
Domain-driven design begins before that structure. It begins when a team tries to understand which problems belong together, what its words mean, where those meanings stop, and which rules must remain true.
Start with language that can disagree
Words reveal boundaries. “Account” may mean a login identity in one part of a system, a billing relationship in another, and a ledger container somewhere else. Forcing those meanings into one universal class creates a model that is technically shared and conceptually unstable.
A ubiquitous language is not a glossary written once. It is the vocabulary used in conversations, examples, tests, APIs, and code inside one context. When the same phrase produces different rules, that tension is evidence that the model may cross a boundary.
A bounded context owns meaning
A bounded context defines where a model applies. It is a semantic boundary before it is a deployment boundary. Two contexts may describe the same real thing differently because they need different facts and enforce different rules.
The contexts exchange explicit contracts rather than sharing one mutable
Account model. They may initially live in the same process. Splitting them
into services is a separate operational decision.
Behavior belongs near the invariant
An anemic model stores data while application services reconstruct every rule. Moving all logic into entities is not automatically better; some policies span several objects or depend on external information. The useful question is where a rule can be expressed with the fewest invalid intermediate states.
public sealed class Membership
{
public MembershipStatus Status { get; private set; }
public void Suspend(SuspensionReason reason)
{
if (Status is MembershipStatus.Closed)
throw new InvalidOperationException(
"A closed membership cannot be suspended.");
Status = MembershipStatus.Suspended;
}
}
The snippet is illustrative and still requires tests. Its point is that the transition protects its invariant. A controller should not be able to assign an arbitrary status and bypass the rule.
Repositories do not create a domain
A repository can present aggregate-oriented persistence operations, but wrapping
every table in IRepository<T> only hides the database behind generic
vocabulary. Query needs, transaction boundaries, and aggregate ownership remain.
Use a repository when it protects domain language or a consistency boundary. Use a direct query abstraction when the job is projection. Avoid returning a composable database query from a boundary that claims to hide persistence.
Folder structure follows change pressure
Once the model has meaningful boundaries, code organization can make them visible. A feature or bounded-context structure often communicates more than global technical buckets:
Authorization/
Policies/
Decisions/
Evaluation/
Persistence/
This is not a required DDD layout. It is one consequence of organizing code around language and change.
DDD has a cost
Domain modeling requires access to knowledge, repeated conversations, and the patience to revise names. For a small CRUD surface with stable rules, a rich model may cost more than it returns. For a domain where policy changes, definitions conflict, and invalid transitions are expensive, the investment can make change safer.
The folder tree is the residue of those decisions, not their source.