What DDD actually means in practice
Domain-Driven Design (DDD) is not a framework and not a set of libraries. It is a way of designing software by starting with the business problem, not the database schema or the API surface.
I did not appreciate DDD when I first read about it. It only clicked when I worked on systems where business rules kept leaking into controllers, services, and stored procedures. Every change felt risky. DDD gave me a way to put rules where they belong and keep them there.
DDD was formalized by Eric Evans in 2003, but the ideas are still very relevant, especially when systems grow beyond a single codebase or team.
What is a domain?
The domain is simply the problem space your software exists in.
Examples:
- Banking: accounts, balances, transactions, limits
- E-commerce: carts, orders, payments, refunds
- Healthcare: patients, appointments, diagnoses
DDD asks you to slow down and understand this space properly before writing code.
The key idea is simple: model the business rules directly in code, using the same language the business uses.
The mindset shift
Before DDD, I often followed a familiar path.
Common approach
- Start with database tables
- Add repositories and services
- Push business rules into controllers or helpers
This works at first, but rules slowly get scattered. Validation appears in multiple places. Invariants are easy to break.
DDD approach
- Start with business rules
- Model them as domain objects
- Keep rules close to the data they protect
Once I made this shift, changes became safer. When rules changed, I knew exactly where to look.
Strategic DDD in plain terms
DDD addresses complexity by breaking large problem spaces into smaller, clearer parts.
Core concepts
-
Domain The overall business problem your system solves.
-
Subdomain A focused area within the domain, with its own rules and goals.
-
Ubiquitous Language A shared language used by developers and domain experts. The same terms appear in conversations, code, and documentation.
-
Bounded Context A clear boundary where a model and its language are consistent. Inside this boundary, words mean one thing. Outside, they might mean something else.
-
Context Mapping How bounded contexts interact with each other.
These ideas helped me stop chasing a single “perfect model” and instead build models that fit their context.
Building blocks of DDD
Before looking at code, it helps to see how the tactical pieces fit together.
DDD uses a small set of building blocks to express business rules clearly.
- Entity
- Value Object
- Aggregate
- Aggregate Root
Entity
An Entity represents something with identity.
- Identity stays the same even when data changes
- Equality is based on ID, not on values
Examples: Customer, Order, Transaction
The key idea is that who it is matters more than what it contains.
Value Object
A Value Object represents a concept defined only by its values.
- No identity
- Usually immutable
- Equality is based on data
Examples: Money, Address, AccountId
If two value objects have the same values, they are the same thing.
Aggregate and Aggregate Root
This was the hardest part for me to internalize.
An Aggregate is a cluster of domain objects that must stay consistent together.
An Aggregate Root is the single entry point into that cluster.
- All changes go through the root
- Internal objects are not modified directly
- Business rules are enforced at the boundary
This structure prevents accidental rule violations and makes consistency explicit.
A banking example from real code
Below is a small, focused example from a banking domain. It shows only the core DDD building blocks.
Value Objects
No identity. Immutable. Compared by value.
public record AccountId(Guid Value);
public record CustomerId(Guid Value);
public record Money(decimal Amount, string Currency)
{
public static Money Zero(string currency) => new(0, currency);
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Currency mismatch");
return new Money(Amount + other.Amount, Currency);
}
public Money Subtract(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Currency mismatch");
if (Amount < other.Amount)
throw new InvalidOperationException("Insufficient balance");
return new Money(Amount - other.Amount, Currency);
}
}
Notice how rules such as currency mismatch and insufficient balance live with the data, not in a service.
Entity
A Transaction has identity and changes over time, but it only makes sense inside an account.
public class Transaction
{
public Guid Id { get; } = Guid.NewGuid();
public Money Amount { get; }
public DateTime OccurredAt { get; }
internal Transaction(Money amount)
{
Amount = amount;
OccurredAt = DateTime.UtcNow;
}
}
Aggregate Root
The BankAccount is the gatekeeper. All rules flow through it.
public class BankAccount
{
private readonly List<Transaction> _transactions = new();
public AccountId Id { get; }
public CustomerId OwnerId { get; }
public Money Balance { get; private set; }
public bool IsClosed { get; private set; }
public IReadOnlyCollection<Transaction> Transactions =>
_transactions.AsReadOnly();
public BankAccount(AccountId id, CustomerId ownerId, string currency)
{
Id = id;
OwnerId = ownerId;
Balance = Money.Zero(currency);
}
public void Deposit(Money amount)
{
EnsureAccountIsActive();
if (amount.Amount <= 0)
throw new InvalidOperationException("Deposit must be positive");
Balance = Balance.Add(amount);
_transactions.Add(new Transaction(amount));
}
public void Withdraw(Money amount)
{
EnsureAccountIsActive();
Balance = Balance.Subtract(amount);
_transactions.Add(new Transaction(
new Money(-amount.Amount, amount.Currency)));
}
public void Close()
{
if (Balance.Amount != 0)
throw new InvalidOperationException("Account balance must be zero");
IsClosed = true;
}
private void EnsureAccountIsActive()
{
if (IsClosed)
throw new InvalidOperationException("Account is closed");
}
}
No controller or service can bypass these rules. That is the real value of an aggregate root.
Aggregate boundary
BankAccount (Aggregate Root)
├── Transaction (Entity)
├── Money (Value Object)
├── AccountId (Value Object)
└── CustomerId (Value Object)
Everything inside this boundary stays consistent together.

What worked better for me
In many systems, business rules are enforced “by convention.” In practice, conventions drift.
With DDD:
- Rules are enforced by the model
- Invalid states are hard to represent
- Changes are localized and safer
Tools and frameworks helped, but they were not the point. The real improvement came from treating the domain model as the center of the system, not as a data container.
When business rules live in the domain model, the system becomes easier to change, easier to reason about, and harder to misuse. That shift, more than any pattern or tool, is what made DDD valuable in my day-to-day work.