Overview
Domain services and domain events are tactical Domain-Driven Design (DDD) patterns used when important domain behavior does not fit cleanly inside one entity or value object.
A domain service represents a meaningful domain operation or policy that involves several domain concepts but is not naturally owned by one of them. It belongs to the domain model and uses the Ubiquitous Language.
A domain event is an immutable statement that something meaningful has already happened in the domain. It allows an aggregate to record a fact without directly coordinating every reaction to that fact.
For example:
- A
PricingPolicycan calculate a price from a product, customer tier, and active promotion. - An
OrderSubmittedevent can trigger inventory reservation, buyer-history updates, or the creation of an integration event.
These patterns matter because business logic commonly becomes scattered between controllers, application services, repositories, message handlers, and entity classes. Domain services provide a home for genuine domain policies that span objects. Domain events make important outcomes and side effects explicit while reducing direct coupling.
They are important in interviews because candidates must distinguish:
- Domain services from application and infrastructure services.
- Events from commands.
- Domain events from integration events.
- Immediate consistency from eventual consistency.
- Event recording from event dispatch.
- Useful decoupling from unnecessary indirection.
Core Concepts
Where Domain Behavior Belongs
Place behavior as close as possible to the state and rules it protects.
Use:
- An entity method when the behavior belongs to one entity and uses its state.
- A value object method when the behavior belongs to a value and can remain immutable.
- An aggregate-root method when the behavior protects aggregate-wide invariants.
- A domain service when the operation is a domain concept but does not belong naturally to one object.
- An application service when the work coordinates a use case, persistence, transactions, or external systems.
- An infrastructure service when the work implements technical details such as SMTP, storage, or an external SDK.
Do not move behavior into a domain service merely because it uses several parameters. First ask whether an entity or value object owns the rule.
What Is a Domain Service?
A domain service is a stateless domain operation expressed using the language of the business.
Typical characteristics:
- It performs domain logic rather than technical orchestration.
- Its name represents a domain concept or policy.
- It consumes and returns domain types.
- It has no identity or independent lifecycle.
- It is usually stateless.
- It does not own transaction or transport concerns.
Examples include:
- Pricing policy.
- Currency conversion policy.
- Loan affordability assessment.
- Route selection policy.
- Funds-transfer policy.
- Tax calculation.
- Scheduling conflict policy.
A domain service is not simply any class whose name ends in Service.
Entity Method Versus Domain Service
Suppose a bank account controls withdrawals:
public sealed class BankAccount
{
public Money Balance { get; private set; }
public Money OverdraftLimit { get; }
public void Withdraw(Money amount)
{
if (amount <= Money.Zero(amount.Currency))
{
throw new DomainRuleViolation(
"Withdrawal amount must be positive.");
}
if (Balance - amount < -OverdraftLimit)
{
throw new DomainRuleViolation(
"The withdrawal exceeds the overdraft limit.");
}
Balance -= amount;
}
}
The rule belongs on BankAccount because that entity owns the balance and overdraft invariant.
A transfer involves two accounts and a transfer policy:
public sealed class FundsTransferService
{
public Transfer Transfer(
BankAccount source,
BankAccount destination,
Money amount,
Instant occurredAt)
{
if (source.Currency != destination.Currency)
{
throw new DomainRuleViolation(
"Accounts must use the same currency.");
}
source.Withdraw(amount);
destination.Deposit(amount);
return Transfer.Record(
source.Id,
destination.Id,
amount,
occurredAt);
}
}
The operation is a recognizable domain concept and no single account should own both sides.
Value Object Versus Domain Service
If a calculation depends only on one value object's state, keep it on that value object:
public sealed record Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
EnsureSameCurrency(other);
return this with { Amount = Amount + other.Amount };
}
}
Use a domain service when a policy combines independent concepts:
public sealed class ExchangeService
{
private readonly IExchangeRateProvider _rates;
public ExchangeService(IExchangeRateProvider rates)
{
_rates = rates;
}
public Money Convert(Money source, Currency target)
{
var rate = _rates.GetRate(source.Currency, target);
return Money.Of(source.Amount * rate, target.Code);
}
}
IExchangeRateProvider is a domain-facing port. Its implementation can call a database or external provider, but the domain service depends only on the capability it needs.
Domain Service Versus Application Service
A domain service makes a business decision or calculation. An application service coordinates a use case.
public sealed class TransferFundsHandler
{
private readonly IAccountRepository _accounts;
private readonly FundsTransferService _transferService;
private readonly IUnitOfWork _unitOfWork;
public async Task<TransferId> Handle(
TransferFunds command,
CancellationToken cancellationToken)
{
var source = await _accounts.GetAsync(
command.SourceAccountId,
cancellationToken);
var destination = await _accounts.GetAsync(
command.DestinationAccountId,
cancellationToken);
var transfer = _transferService.Transfer(
source,
destination,
command.Amount,
SystemClock.Instance.GetCurrentInstant());
await _unitOfWork.SaveChangesAsync(cancellationToken);
return transfer.Id;
}
}
The application handler:
- Loads aggregates.
- Defines the transaction.
- Invokes domain behavior.
- Saves changes.
- Handles authorization and external workflow concerns.
The domain service:
- Applies the transfer policy.
- Uses domain types.
- Does not know about HTTP, EF Core, or message brokers.
Domain Service Versus Infrastructure Service
An infrastructure service implements a technical capability:
public sealed class SendGridEmailSender : IEmailSender
{
public Task SendAsync(
EmailMessage message,
CancellationToken cancellationToken)
{
// Vendor SDK call.
}
}
The domain may decide that a customer should be notified. Infrastructure decides how to send the notification.
Names such as EmailService, FileService, and CacheService usually describe technical services, not domain services.
Keeping Domain Services Focused
A domain service should have one coherent domain responsibility.
Prefer:
PricingPolicy
CreditEligibilityPolicy
ShipmentRoutingPolicy
Avoid:
OrderService
BusinessService
DomainManager
Generic service names often accumulate unrelated logic and recreate an anemic domain model.
If a domain service repeatedly mutates the internals of one entity, move that behavior into the entity. If it mainly loads repositories and sends messages, it is probably an application service.
Pure and Impure Domain Services
A pure domain service calculates from supplied domain values and has no external dependencies:
public sealed class DiscountPolicy
{
public Percentage Calculate(
CustomerTier tier,
Money basketTotal)
{
if (tier == CustomerTier.Platinum &&
basketTotal.Amount >= 500m)
{
return Percentage.Of(10);
}
return Percentage.Zero;
}
}
An impure domain service may need information that is not already loaded:
public interface ICreditExposure
{
Money CurrentExposure(CustomerId customerId);
}
public sealed class CreditApprovalPolicy
{
private readonly ICreditExposure _exposure;
public CreditApprovalPolicy(ICreditExposure exposure)
{
_exposure = exposure;
}
public bool CanApprove(
Customer customer,
Money requestedCredit)
{
var current = _exposure.CurrentExposure(customer.Id);
return current + requestedCredit <= customer.CreditLimit;
}
}
Keep the dependency expressed as a domain capability. Avoid injecting DbContext, HTTP clients, or vendor SDKs into the domain layer.
What Is a Domain Event?
A domain event is an immutable description of a domain fact that has already occurred.
Examples:
OrderSubmitted.PaymentCaptured.SubscriptionExpired.InventoryReserved.LoanApplicationApproved.
Events should:
- Use past tense.
- Use the Ubiquitous Language.
- Contain enough information for intended handlers.
- Be immutable.
- Represent something meaningful to the domain.
- Record facts rather than requests.
public sealed record OrderSubmittedDomainEvent(
OrderId OrderId,
CustomerId CustomerId,
Money Total,
Instant SubmittedAt) : IDomainEvent;
Commands Versus Events
A command requests an action:
SubmitOrder
CapturePayment
CancelSubscription
An event reports an outcome:
OrderSubmitted
PaymentCaptured
SubscriptionCancelled
Important differences:
Do not name an event SubmitOrderEvent. That is a command disguised as an event.
Recording Events Inside Aggregates
An aggregate can record events when domain behavior succeeds:
public abstract class AggregateRoot
{
private readonly List<IDomainEvent> _domainEvents = [];
public IReadOnlyCollection<IDomainEvent> DomainEvents =>
_domainEvents.AsReadOnly();
protected void Raise(IDomainEvent domainEvent) =>
_domainEvents.Add(domainEvent);
public void ClearDomainEvents() =>
_domainEvents.Clear();
}
public sealed class Order : AggregateRoot
{
public void Submit(Instant submittedAt)
{
EnsureCanSubmit();
Status = OrderStatus.Submitted;
SubmittedAt = submittedAt;
Raise(new OrderSubmittedDomainEvent(
Id,
CustomerId,
CalculateTotal(),
submittedAt));
}
}
Recording does not necessarily dispatch the event immediately. The aggregate remains focused on its state transition.
Immediate Versus Deferred Dispatch
Immediate dispatch invokes handlers as soon as the event is raised. It can make side effects happen during an aggregate method, which complicates testing, transaction reasoning, and error handling.
Deferred dispatch records events and publishes them near the unit-of-work boundary. This separates the domain decision from side-effect execution.
A common flow is:
Command handler
-> load aggregate
-> invoke domain behavior
-> aggregate records events
-> unit of work dispatches events
-> persist changes
-> clear recorded events
Deferred dispatch makes the transaction policy explicit and is generally easier to test.
Dispatch Before or After Commit
Dispatching events before the database commit can include handler changes in the same local transaction:
Change aggregate
-> dispatch domain handlers
-> save all changes
-> commit once
Advantages:
- Atomic local side effects.
- Straightforward rollback.
- Simpler consistency model.
Costs:
- A larger transaction.
- More locks and coupling between handlers.
- Slow handlers extend transaction time.
Dispatching events after commit separates transactions:
Commit aggregate
-> dispatch handlers
-> handlers commit separately
Advantages:
- Smaller original transaction.
- Better isolation of independent reactions.
Costs:
- Eventual consistency.
- A process crash can occur after commit but before dispatch.
- Retries, idempotency, and durable event storage may be required.
The correct choice follows business consistency and reliability requirements.
Domain Event Handlers
Handlers react to domain events. They commonly live in the application layer because they may:
- Load another aggregate.
- Invoke a repository.
- Start an external workflow.
- Create an integration event.
- Send a notification through a port.
public sealed class ReserveInventoryWhenOrderSubmitted
: IDomainEventHandler<OrderSubmittedDomainEvent>
{
private readonly IInventoryRepository _inventory;
public async Task Handle(
OrderSubmittedDomainEvent domainEvent,
CancellationToken cancellationToken)
{
var reservation = await _inventory.GetForOrderAsync(
domainEvent.OrderId,
cancellationToken);
reservation.Reserve();
}
}
The handler coordinates the reaction, while the loaded aggregate protects its own rules.
Domain Events Versus Integration Events
Both represent facts, but they have different boundaries and delivery requirements.
Domain events:
- Are internal to a bounded context.
- Often run in-process.
- Can contain domain-specific types.
- Can evolve with the internal model.
- May participate in a local transaction.
Integration events:
- Cross process or bounded-context boundaries.
- Are asynchronous contracts.
- Must be serializable and versionable.
- Should contain stable, consumer-safe data.
- Must be published only for committed state.
- Require delivery, retry, and observability mechanisms.
Do not publish internal domain objects directly:
public sealed record OrderSubmittedIntegrationEvent(
Guid OrderId,
Guid CustomerId,
decimal Total,
string Currency,
DateTimeOffset SubmittedAt);
An application handler can translate a domain event into this public contract.
The Transactional Outbox
If state is committed and an integration event must be published reliably, saving the database change and publishing to a broker are two separate operations.
Failure scenario:
1. Database commit succeeds.
2. Process crashes.
3. Broker publish never occurs.
The transactional outbox addresses this:
One database transaction:
- Save aggregate changes.
- Save an outbox message.
Background publisher:
- Read unpublished messages.
- Publish to broker.
- Mark messages as published.
The outbox provides at-least-once delivery in common implementations. Consumers must therefore be idempotent.
Idempotency and Duplicate Delivery
Distributed messaging cannot generally promise that a handler runs exactly once from the business perspective. Retries can produce duplicate delivery.
An idempotent consumer can:
- Store processed message IDs.
- Use a unique business key.
- Make the state transition conditional.
- Use an inbox table.
- Treat repeated facts as no-ops.
if (await _inbox.HasProcessedAsync(
message.Id,
cancellationToken))
{
return;
}
await HandleBusinessOperation(message, cancellationToken);
await _inbox.MarkProcessedAsync(message.Id, cancellationToken);
The inbox update and business change should be atomic where practical.
Ordering and Stale Events
Events can arrive out of order:
OrderSubmitted version 3
OrderCancelled version 4
OrderAddressChanged version 2
Possible strategies include:
- Aggregate sequence numbers.
- Expected-version checks.
- Per-key ordered partitions.
- Ignoring stale versions.
- Designing commutative handlers.
- Reconciliation jobs.
Do not assume global event ordering unless the infrastructure and design explicitly provide it.
Domain Events and Eventual Consistency
Domain events can coordinate reactions across aggregates:
Order.Submit()
-> OrderSubmitted
-> reserve inventory
-> update customer history
If handlers execute in separate transactions, temporary inconsistency exists. The design must specify:
- Acceptable delay.
- Retry policy.
- Failure visibility.
- Compensation.
- Idempotency.
- Manual recovery.
Eventual consistency is a business behavior, not merely a technical implementation detail.
Event Cascades
One event handler can change another aggregate, which raises another event:
OrderSubmitted
-> InventoryReserved
-> ShipmentRequested
Long cascades make control flow difficult to understand and can create loops.
Keep event chains manageable by:
- Modeling a process manager or saga for long workflows.
- Making workflow state explicit.
- Limiting hidden synchronous cascades.
- Adding correlation and causation IDs.
- Monitoring event flow.
- Detecting cycles.
Domain Events Are Not Event Sourcing
Using domain events does not mean the application is event-sourced.
With ordinary domain events:
- Current state is persisted normally.
- Events communicate completed facts or trigger side effects.
- The event list may be transient or stored only for delivery.
With event sourcing:
- The event stream is the source of truth.
- Aggregate state is rebuilt by replaying events.
- Event schema evolution and projection rebuilding are core concerns.
The patterns can coexist, but they solve different problems.
Testing Domain Services
Pure domain services are easy to test:
[Fact]
public void Platinum_customer_receives_discount_for_large_basket()
{
var policy = new DiscountPolicy();
var discount = policy.Calculate(
CustomerTier.Platinum,
Money.Usd(600m));
discount.Should().Be(Percentage.Of(10));
}
Tests should cover:
- Policy boundaries.
- Invalid combinations.
- Relevant value-object behavior.
- Interaction with domain ports where necessary.
Avoid tests that only verify that a method was called. Verify the business result.
Testing Domain Events
Aggregate tests can verify recorded facts:
[Fact]
public void Submitting_an_order_records_the_domain_event()
{
var order = OrderBuilder.ValidDraft();
var now = Instant.FromUtc(2026, 6, 14, 10, 0);
order.Submit(now);
order.DomainEvents.Should().ContainSingle()
.Which.Should().BeOfType<OrderSubmittedDomainEvent>();
}
Also test:
- Event payload values.
- Events are not raised when an operation fails.
- Handler effects.
- Transaction timing.
- Duplicate delivery.
- Outbox publication and retry.
- Contract compatibility for integration events.
When Not to Use a Domain Service
Avoid a domain service when:
- The behavior clearly belongs to an entity or value object.
- The class only forwards to a repository.
- It only maps DTOs.
- It coordinates HTTP, persistence, and transactions.
- It is a generic container for all business logic.
- The context is simple CRUD with no meaningful domain policy.
When Not to Use a Domain Event
Avoid a domain event when:
- A direct method call is clearer.
- The caller requires an immediate result from one known collaborator.
- The occurrence has no domain significance.
- The event only reports a property setter.
- It hides a required synchronous invariant.
- The team cannot operate the resulting asynchronous workflow reliably.
Events trade direct coupling for indirect control flow and operational concerns. Use that trade deliberately.
Common Mistakes
- Moving all entity behavior into domain services.
- Naming technical helpers as domain services.
- Injecting
DbContext, HTTP clients, or message brokers into the domain model. - Using commands and events interchangeably.
- Naming events in the present or imperative tense.
- Making events mutable.
- Publishing integration events before the transaction commits.
- Assuming event delivery is exactly once.
- Dispatching immediately from static global infrastructure.
- Hiding mandatory consistency behind asynchronous handlers.
- Creating long, invisible event cascades.
- Putting every side effect in the aggregate itself.
- Treating domain events as event sourcing.
- Adding events where a direct call is easier to understand.
Best Practices
- Keep behavior on entities and value objects when they naturally own it.
- Name domain services after specific domain policies.
- Keep domain services stateless and independent of infrastructure.
- Model events as immutable past-tense facts.
- Record events in aggregates and dispatch them at an explicit unit-of-work boundary.
- Decide before-commit versus after-commit behavior from consistency requirements.
- Keep handlers small and single-purpose.
- Translate internal domain events into stable integration contracts.
- Use an outbox when committed facts must be published reliably.
- Design consumers for duplicate and out-of-order delivery.
- Make eventual consistency, retries, and recovery visible to the business and operations.
- Test domain decisions separately from delivery infrastructure.