Loading learning content...
In the previous page, we established when logic doesn't belong to entities. But recognizing that you need a service isn't the same as building a proper Domain Service.
The term 'service' is dangerously overloaded in software development. We have web services, microservices, application services, infrastructure services, and now domain services. Without clear definitions, developers create arbitrary service classes that violate DDD principles while bearing the 'domain service' label.
A genuine Domain Service isn't just 'a service that does domain stuff.' It has specific, defining characteristics that distinguish it from other service types and ensure it remains a coherent part of your domain model.
By the end of this page, you will understand the five defining characteristics of Domain Services: domain concept expression, statelessness, operation-centric design, interface as part of the domain model, and use of Ubiquitous Language. You'll be able to evaluate whether a service qualifies as a Domain Service.
The first and most fundamental characteristic: a Domain Service represents something the domain experts recognize and name.
This isn't a technical construct invented by developers—it's a genuine concept from the problem domain. When you ask a domain expert, they should be able to explain what this operation is and why it matters to the business.
Examples of domain concepts as services:
| Domain | Domain Concept | Why It's a Domain Concept |
|---|---|---|
| Banking | Funds Transfer | Compliance regulations, banking procedures, audit requirements all reference 'transfers' as a distinct operation |
| Insurance | Risk Assessment | Actuaries and underwriters speak of 'assessing risk' as a specific professional practice |
| Healthcare | Drug Interaction Check | Medical professionals perform interaction checks; it's a recognized clinical procedure |
| Logistics | Route Optimization | Fleet managers conceptualize 'route optimization' as a planning activity |
| Trading | Order Matching | Traders and exchange operators understand 'matching' as the core market mechanism |
The litmus test:
Ask yourself: "Would a domain expert understand what this service does just from its name?" If the answer is yes, you likely have a genuine domain concept. If the answer is no—if the name is technical jargon like DataProcessor or HelperService—it's probably not a Domain Service.
OrderValidator — Too technicalDataTransformer — Infrastructure concernUtilityHelper — No domain meaningProcessManager — Generic, not domain-specificEntityHandler — Technical pattern nameOrderFulfillmentService — Domain conceptPremiumCalculator — Domain operationFundsTransferService — Business operationTradeMatchingEngine — Market mechanismCreditScoreEvaluator — Domain assessmentWhen introducing a new Domain Service, describe it to a domain expert. If they nod and say 'Yes, that's how we do X,' you've captured a real domain concept. If they look confused by your technical description, you may have created technical infrastructure misnamed as a domain service.
Domain Services are stateless. They do not hold conversational state between method invocations. Each call is independent—given the same inputs, a Domain Service produces the same outputs.
This is a crucial distinction from entities, which are fundamentally about state. An entity's identity persists across time; its methods modify its internal state. A Domain Service, by contrast, is a pure operation.
Why statelessness matters:
123456789101112131415161718192021222324252627282930313233343536373839
// ✅ STATELESS Domain Serviceclass TaxCalculationService { // No instance fields storing state constructor( private readonly taxRateRepository: TaxRateRepository, // Dependency, not state private readonly taxRuleEngine: TaxRuleEngine // Dependency, not state ) {} calculateTax(order: Order, customer: Customer): TaxResult { // All inputs come from parameters or dependencies // No instance state is read or modified const rates = this.taxRateRepository.getRatesFor(customer.jurisdiction); const rules = this.taxRuleEngine.getRulesFor(order.productCategories); return this.computeTax(order.lineItems, rates, rules); } private computeTax(items: OrderItem[], rates: TaxRate[], rules: TaxRule[]): TaxResult { // Pure calculation - same inputs always produce same outputs }} // ❌ STATEFUL Service - NOT a proper Domain Serviceclass StatefulCalculator { private lastCalculation: TaxResult; // ❌ Instance state private calculationCount: number = 0; // ❌ Instance state calculateTax(order: Order, customer: Customer): TaxResult { this.calculationCount++; // ❌ Modifying state this.lastCalculation = /* ... */; // ❌ Storing state return this.lastCalculation; } getLastCalculation(): TaxResult { // ❌ State-dependent method return this.lastCalculation; }}Constructor-injected dependencies (repositories, other services, configuration) are NOT state in this context. They're collaborators that the service uses to perform its operation. State would be instance fields that change between method calls and affect the service's behavior based on past interactions.
While entities are defined by identity ('this is customer #12345') and value objects are defined by attributes ('$100 USD'), Domain Services are defined by operations ('transfer funds from A to B').
A Domain Service is essentially a named, encapsulated operation—or a cohesive set of related operations—that exists in the domain.
The verb-centric nature:
Domain Services often map to verbs in the Ubiquitous Language, while entities and value objects map to nouns. When domain experts talk about actions, processes, and transformations that don't 'belong' to a single entity, they're often describing Domain Service candidates.
| Domain Grammar | DDD Building Block | Examples |
|---|---|---|
| Nouns (identity) | Entity | Customer, Order, Account, Product |
| Nouns (descriptive) | Value Object | Money, Address, DateRange, Quantity |
| Verbs (domain operations) | Domain Service | Transfer, Match, Calculate, Validate |
1234567891011121314151617181920212223242526272829303132333435
// Operation-centric Domain Service interface// Note: Named after the OPERATION (verb), not the data (noun) interface FundsTransferService { // Primary operation - the reason this service exists transfer( source: AccountId, destination: AccountId, amount: Money, reference: TransferReference ): TransferResult;} interface OrderMatchingService { // Primary operation matchOrder( incomingOrder: Order, orderBook: OrderBook ): MatchingResult;} interface ShippingCostCalculator { // Primary operation calculateShippingCost( package_: Package, origin: Address, destination: Address, options: ShippingOptions ): ShippingQuote;} // Contrast with data-centric (anti-pattern) naming:// ❌ AccountService - what operations? too vague// ❌ OrderManager - generic, not domain-specific// ❌ ShippingHelper - 'helper' is a code smellSingle Responsibility for Services:
Just as entities should have a cohesive identity, Domain Services should have a cohesive operational purpose. A service named FundsTransferService should focus on funds transfer operations. It shouldn't also handle account creation, fraud detection, and reporting—those are separate domain concepts.
Resist the temptation to add unrelated operations to an existing service 'because the code is nearby.' Each Domain Service should represent a single domain concept. When you find unrelated operations, create a new service—it costs almost nothing but dramatically improves clarity.
A crucial architectural characteristic: the Domain Service interface is part of the domain model itself.
This means the interface lives in the domain layer alongside entities, value objects, and repositories. The interface is defined using domain types and expresses domain concepts.
Why this matters architecturally:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// Domain Layer (domain/services/funds-transfer-service.ts)// ============================================// Interface is IN the domain layer export interface FundsTransferService { /** * Transfers funds between accounts, applying all business rules * for transfer limits, overdraft protection, and compliance. */ transfer( source: AccountId, destination: AccountId, amount: Money, reference: TransferReference ): TransferResult;} // Domain types used - no infrastructure leakageexport interface TransferResult { readonly transferId: TransferId; readonly status: TransferStatus; readonly executedAt: Timestamp; readonly fees: Money;} export type TransferStatus = | 'COMPLETED' | 'PENDING_APPROVAL' | 'REJECTED' | 'REQUIRES_VERIFICATION'; // Infrastructure/Application Layer (infrastructure/services/...)// ============================================// Implementation MAY live outside domain layer import { FundsTransferService, TransferResult } from '@domain/services'; export class FundsTransferServiceImpl implements FundsTransferService { constructor( private accountRepository: AccountRepository, private transferRulesEngine: TransferRulesEngine, private eventPublisher: DomainEventPublisher ) {} transfer( source: AccountId, destination: AccountId, amount: Money, reference: TransferReference ): TransferResult { // Implementation details... // May use infrastructure concerns internally }}The Dependency Inversion Principle in action:
By defining the interface in the domain layer, we achieve proper dependency inversion:
This structure enables the domain layer to remain pure and testable, while implementations can use whatever infrastructure they need.
Some Domain Services have purely algorithmic implementations (pricing calculations, matching algorithms) that can live entirely in the domain layer. Others require infrastructure access (external service calls, database queries) and their implementations belong in the infrastructure layer. The interface always stays in domain.
This characteristic ties everything together: Domain Services express themselves entirely in the Ubiquitous Language.
Every aspect of a Domain Service—its name, its method names, its parameter types, its return types, its documentation—should use terms from the shared vocabulary that developers and domain experts have established.
Why language precision matters:
1234567891011121314151617181920212223242526272829303132333435363738
// ❌ POOR: Technical/generic languageinterface GenericService { process(data: ProcessData): ProcessResult; validate(input: string): boolean; updateStatus(id: number, status: number): void;} // ✅ GOOD: Ubiquitous Language from insurance domaininterface ClaimAdjudicationService { /** * Adjudicates a submitted insurance claim, applying coverage rules * and determining the payout. * * @param claim - The submitted claim to adjudicate * @param policy - The policy under which the claim is made * @returns Adjudication decision including payout determination */ adjudicateClaim( claim: InsuranceClaim, policy: InsurancePolicy ): AdjudicationDecision;} interface AdjudicationDecision { readonly claimId: ClaimId; readonly verdict: ClaimVerdict; // 'APPROVED' | 'DENIED' | 'PENDING_REVIEW' readonly approvedAmount: Money | null; readonly denialReasons: DenialReason[]; readonly requiredDocuments: DocumentType[]; readonly adjudicatedBy: AdjudicatorId; readonly adjudicatedAt: Timestamp;} // Domain experts immediately recognize:// - "Adjudication" is their term for claim evaluation// - "Verdict" is how they describe decisions// - "Denial Reasons" are a regulatory requirement// - The process captures real business workflowNaming examples across domains:
| Domain | Technical Name (Avoid) | Ubiquitous Language (Prefer) |
|---|---|---|
| Banking | performTransaction() | transferFunds() / executeDomesticWire() |
| Healthcare | processMedicalData() | generateDiagnosis() / prescribeTreatment() |
| E-commerce | calculateNumbersForOrder() | calculateOrderTotal() / applyPromotionalPricing() |
| Shipping | runAlgorithm() | optimizeDeliveryRoute() / estimateShipmentArrival() |
| Insurance | checkRequirements() | underwritePolicy() / assessRisk() |
Terms like 'process,' 'handle,' 'manage,' 'execute,' 'perform,' and 'run' are red flags. They're generic technical verbs that could mean anything. Replace them with domain-specific verbs that communicate actual business meaning.
Let's consolidate all five characteristics into a reference that you can use to evaluate potential Domain Services:
| Characteristic | Definition | Validation Question |
|---|---|---|
| Domain Concept | Represents a recognizable domain operation | Would a domain expert recognize this term? |
| Stateless | No conversational state between calls | Can this be a singleton without thread-safety issues? |
| Operation-Centric | Defined by what it does, not what it holds | Is this named after an operation (verb) not a thing (noun)? |
| Interface in Domain | Contract defined in domain layer | Does the interface live alongside entities and value objects? |
| Ubiquitous Language | Uses shared vocabulary throughout | Can domain experts understand the method signatures? |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
/** * Complete Domain Service Example: Trade Matching * * ✅ Domain Concept: "Order matching" is how traders describe the core market mechanism * ✅ Stateless: No instance state, just operations * ✅ Operation-Centric: Named for what it does (matching), not what it is * ✅ Interface in Domain: Lives in domain/services/ * ✅ Ubiquitous Language: Order, Trade, Fill, Match - all trader terminology */ // File: domain/services/order-matching-service.ts export interface OrderMatchingService { /** * Matches an incoming order against resting orders in the book. * Uses price-time priority for matching determination. * * @param incomingOrder - The new order to match * @param orderBook - Current state of resting orders * @returns Matching results including any trades executed */ matchOrder(incomingOrder: Order, orderBook: OrderBook): MatchingResult; /** * Attempts to cross orders within the same participant (internal crossing). * Must apply fair pricing rules for internal matches. */ attemptInternalCross( buyOrder: Order, sellOrder: Order, crossingRules: CrossingRules ): CrossResult;} export interface MatchingResult { readonly incomingOrderId: OrderId; readonly trades: Trade[]; readonly fills: Fill[]; readonly remainingQuantity: Quantity; readonly matchingType: 'FULL' | 'PARTIAL' | 'NONE';} export interface Trade { readonly tradeId: TradeId; readonly buyOrderId: OrderId; readonly sellOrderId: OrderId; readonly quantity: Quantity; readonly price: Price; readonly executedAt: Timestamp;} export interface Fill { readonly orderId: OrderId; readonly filledQuantity: Quantity; readonly averagePrice: Price;}We've established the five defining characteristics that distinguish a proper Domain Service from arbitrary service classes.
What's next:
Now that we understand what makes a Domain Service, we need to distinguish it from another common pattern: the Application Service. The difference is subtle but crucial—confusing them leads to design problems. The next page explores this distinction in depth.
You can now identify and evaluate Domain Services based on their defining characteristics. Next, we'll distinguish Domain Services from Application Services—a distinction that determines where your business logic truly belongs.