Imagine walking into an office to request some data.
In one building, there is no manager. You enter, search files, validate rules, double-check policies, and repeat work that others already did.
Without DDD
With the help of domain manager
In another building, there is a manager. You simply ask. The manager knows the rules, validates everything once, and hands you trusted information.
Why DDD Was Introduced
Before Domain-Driven Design, many systems looked like an office with no manager. Business rules were scattered across controllers, services, UI, and databases. The same rule was often repeated in multiple places.
At first, this worked because systems were small and easy to understand. But as they grew, problems appeared:
- A rule changed, but only some copies were updated
- Old logic stayed hidden in forgotten files
- Developers didn’t know which version was correct
- Bugs appeared in unexpected places
Teams spent more time searching for rules than building features.
Eric Evans realized the real issue wasn’t bad code. It was that business logic had no clear home. No single place was responsible for saying what was allowed and what was not.
So he introduced Domain-Driven Design to give business rules a clear owner: the domain.
With DDD, important entities must go through the domain. The domain acts like a manager:
- It validates rules
- It protects important states
- It prevents invalid data
- It fails early when something is wrong
This makes systems easier to understand, change, and trust.
Best Practices
1. Keep Domain Services Pure and Focused on Business Rules
Domain services should only contain business logic. They should not be responsible for application-level concerns like database transactions, authorization, or fetching entities from a repository.
// Good - Pure rule: receives aggregates already loaded.
public class MoneyTransferManager : DomainService
{
public void Transfer(Account from, Account to, decimal amount)
{
from.Withdraw(amount);
to.Deposit(amount);
}
}
// Bad - Mixing application and domain concerns.
// This logic belongs in an Application Service.
public class MoneyTransferManager : DomainService
{
private readonly IRepository<Account, Guid> _accountRepository;
public MoneyTransferManager(IRepository<Account, Guid> accountRepository)
{
_accountRepository = accountRepository;
}
public async Task TransferAsync(Guid fromId, Guid toId, decimal amount)
{
var from = await _accountRepository.GetAsync(fromId);
var to = await _accountRepository.GetAsync(toId);
from.Withdraw(amount);
to.Deposit(amount);
}
}
2. Leverage Entity Methods First
Prefer encapsulating business logic within an entity’s methods when the logic belongs to a single aggregate. Use a domain service only when a business rule spans multiple aggregates.
// Good - Internal state change belongs in the entity
public class Account : AggregateRoot<Guid>
{
public decimal Balance { get; private set; }
public void Withdraw(decimal amount)
{
if (Balance < amount)
throw new BusinessException("Insufficient balance");
Balance -= amount;
}
}
// Use Domain Service only when logic spans multiple aggregates
public class MoneyTransferManager : DomainService
{
public void Transfer(Account from, Account to, decimal amount)
{
from.Withdraw(amount);
to.Deposit(amount);
}
}
3. Prefer Domain Services over Anemic Entities
Avoid placing business logic that coordinates multiple entities directly into an application service. This leads to an “Anemic Domain Model,” where entities are just data bags and logic is scattered in application services.
// Bad - Business logic in Application Service (Anemic Domain)
public class BankAppService : ApplicationService
{
public async Task TransferAsync(Guid fromId, Guid toId, decimal amount)
{
var from = await _accountRepository.GetAsync(fromId);
var to = await _accountRepository.GetAsync(toId);
// This is domain logic and should be in a Domain Service
if (ReferenceEquals(from, to))
throw new BusinessException("Cannot transfer to the same account.");
if (amount <= 0)
throw new BusinessException("Transfer amount must be positive.");
from.Withdraw(amount);
to.Deposit(amount);
}
}
4. Use Meaningful Names
ABP recommends naming domain services with a Manager or Service suffix based on the business concept they represent.
// Good
MoneyTransferManager
OrderManager
IssueManager
InventoryAllocationService
// Bad
AccountHelper
OrderProcessor
Advanced Example: Order Processing with Inventory Check
// Domain abstraction - implementation is in infrastructure
public interface IInventoryChecker : IDomainService
{
Task<bool> IsAvailableAsync(Guid productId, int quantity);
}
public class OrderManager : DomainService
{
private readonly IInventoryChecker _inventoryChecker;
public OrderManager(IInventoryChecker inventoryChecker)
{
_inventoryChecker = inventoryChecker;
}
public async Task ProcessAsync(Order order, Inventory inventory)
{
foreach (var item in order.Items)
{
if (!await _inventoryChecker.IsAvailableAsync(item.ProductId, item.Quantity))
throw new BusinessException(L["InsufficientInventory", item.ProductId]);
}
foreach (var item in order.Items)
{
inventory.Reserve(item.ProductId, item.Quantity);
}
order.SetStatus(OrderStatus.Processing);
}
}
Domain abstractions: The IInventoryChecker interface is a domain service contract. Its implementation can live in the infrastructure layer, but the contract belongs to the domain. This keeps the domain independent of infrastructure while allowing complex validations.
Caution: Validate and perform actions atomically within a single transaction to avoid race conditions (TOCTOU—Time Of Check Time Of Use). When a domain service coordinates multiple aggregates, ensure the Application Service wraps the operation in a Unit of Work. ABP’s [UnitOfWork] attribute or built-in UoW handling does this automatically.
Common Pitfalls and How to Avoid Them
1. Bloated Domain Services
Don’t let domain services become “god objects.” Keep them focused on a single business concept.
// Bad - Too many responsibilities
public class AccountManager : DomainService
{
public void Transfer(Account from, Account to, decimal amount) { }
public void CalculateInterest(Account account) { }
public void GenerateStatement(Account account) { }
public void ValidateAddress(Account account) { }
public void SendNotification(Account account) { }
}
// Good - Split by business concept
public class MoneyTransferManager : DomainService
{
public void Transfer(Account from, Account to, decimal amount) { }
}
public class InterestCalculationManager : DomainService
{
public void Calculate(Account account) { }
}
2. Circular Dependencies Between Aggregates
When domain services coordinate multiple aggregates, avoid circular dependencies. Consider domain events instead of direct coupling.
public class OrderManager : DomainService
{
public async Task ProcessAsync(Order order)
{
order.SetStatus(OrderStatus.Processing);
await LocalEventBus.PublishAsync(new OrderProcessedEvent
{
OrderId = order.Id,
CustomerId = order.CustomerId
});
}
}
3. Confusing Domain Service with Domain Event Handlers
Domain services orchestrate business operations. Domain event handlers react to state changes. Don’t mix them.
// Domain Service - Orchestrates business logic
public class MoneyTransferManager : DomainService
{
public async Task TransferAsync(Account from, Account to, decimal amount)
{
from.Withdraw(amount);
to.Deposit(amount);
await LocalEventBus.PublishAsync(new MoneyTransferredEvent
{
FromAccountId = from.Id,
ToAccountId = to.Id,
Amount = amount
});
}
}
// Domain Event Handler - Reacts to domain events
public class MoneyTransferredEventHandler :
ILocalEventHandler<MoneyTransferredEvent>,
ITransientDependency
{
public async Task HandleEventAsync(MoneyTransferredEvent eventData)
{
// Send notification, update analytics, etc.
}
}
How DDD Simplifies Business Logic and Speeds Up Development
With DDD, business rules stop being scattered. They get one home: the domain.
Validation, calculations, and invariants live inside entities, aggregates, and domain services. Controllers and application services no longer re-check rules. They ask the domain and trust the result.
Entities protect themselves. They don’t allow invalid states. Callers don’t need to “be careful”—the domain enforces correctness.
When a rule changes, there is one place to update. No searching. No forgotten copies.
Because business logic is centralized:
- Rule changes happen in one place
- New features reuse existing behavior
- New developers understand the system by reading the domain
- Domain logic can be tested without HTTP, databases, or UI
- Centralized validation reduces bugs
At first, DDD may feel slower because of modeling and design. But once the “manager” exists, development becomes calmer and faster.
Like in the office metaphor: you ask once, get a reliable answer, and move on.
Conclusion: Start With One Bounded Context
DDD is most valuable when rules are complex, shared, and changing.
It is not needed for simple CRUD with basic validation.
You don’t need to apply it everywhere. Start where duplicated rules and confusion hurt the most. Build one bounded context with strong aggregates and domain services, and let the rest of the system call into it.
Once that manager is in place, you’ll write less code, make fewer mistakes, and scale more confidently.