DDD Services
Service
In Domain-Driven Design (DDD), a Service is a design pattern used to encapsulate domain logic that doesn’t naturally fit into a specific entity or value object. Services help maintain the Single Responsibility Principle by keeping domain entities focused on modeling the core domain, while services handle cross-cutting or domain-specific operations that involve multiple entities.
Characteristics of a Service in DDD:
-
Operation-Focused: Services encapsulate behavior, not data. They typically perform actions or operations within the domain model.
-
Stateless: Services are often stateless, meaning they don’t maintain state between calls. They receive data, perform operations, and return results, without holding onto internal state.
-
Domain Logic Outside Entities: If a domain operation spans multiple aggregates, entities, or external systems, it may be best represented as a service rather than embedded within individual entities.
-
Three Types of Services:
- Domain Services: Implement business logic that doesn’t belong in a specific entity or aggregate.
- Application Services: Coordinate between domain services and repositories, often representing use cases or application workflows.
- Infrastructure Services: Handle technical or cross-cutting concerns like sending emails, logging, or interacting with external APIs.
Example of Services in Domain-Driven Design (TypeScript)
Let’s extend our e-commerce example to include services that perform certain operations. We’ll use a domain service to calculate a discount for an order.
1. Defining a Domain Service
Suppose we need to calculate a discount for an order based on some business rules. This logic doesn’t naturally belong in the Order entity itself, since discounts might involve external systems (e.g., checking customer loyalty status). So, we’ll encapsulate this logic in a domain service.
class DiscountService {
calculateDiscount(order: Order, customer: Customer): number {
let discount = 0;
// Business rule: loyal customers get a 10% discount
if (customer.isLoyal()) {
discount += order.getTotalAmount() * 0.1;
}
// Business rule: orders above $500 get a 5% discount
if (order.getTotalAmount() > 500) {
discount += order.getTotalAmount() * 0.05;
}
return discount;
}
}Here:
DiscountServiceis a domain service that encapsulates business logic around calculating discounts for an order.- The service does not belong to any particular entity like
OrderorCustomer, because the logic spans both.
2. Using the Domain Service
Let’s see how the DiscountService can be used in the application logic:
async function placeOrderWithDiscount(
orderRepository: OrderRepository,
customer: Customer,
products: Product[],
quantities: number[]
) {
const orderFactory = new OrderFactory();
const order = orderFactory.createOrder(customer, products, quantities);
const discountService = new DiscountService();
const discount = discountService.calculateDiscount(order, customer);
const totalAmount = order.getTotalAmount() - discount;
console.log(`Total order amount after discount: $${totalAmount}`);
await orderRepository.add(order);
}
// Example usage:
await placeOrderWithDiscount(orderRepository, customer, [product1, product2], [2, 1]);In this example:
- The
DiscountServiceis used to calculate any applicable discount based on business rules. - The domain service is called during the order placement process, ensuring that the discount logic is encapsulated outside the
Orderentity, promoting separation of concerns.
3. Application Services
In DDD, application services are responsible for coordinating use cases or workflows within the application. They interact with domain services, repositories, and other components but typically don’t contain business logic themselves.
An application service might look like this:
class OrderApplicationService {
constructor(
private readonly orderRepository: OrderRepository,
private readonly discountService: DiscountService
) {}
async placeOrder(customer: Customer, products: Product[], quantities: number[]): Promise<void> {
const orderFactory = new OrderFactory();
const order = orderFactory.createOrder(customer, products, quantities);
// Calculate the discount using the domain service
const discount = this.discountService.calculateDiscount(order, customer);
// Final amount after discount
const totalAmount = order.getTotalAmount() - discount;
console.log(`Placing order with total amount: $${totalAmount}`);
// Save the order using the repository
await this.orderRepository.add(order);
}
}Here:
OrderApplicationServicecoordinates the high-level task of placing an order. It uses theOrderRepositoryto persist the order and theDiscountServiceto calculate the discount.- The business logic (e.g., discount calculation) is delegated to a domain service (
DiscountService), while the application service focuses on orchestrating the workflow.
4. Infrastructure Services
Infrastructure services handle concerns like sending notifications, logging, interacting with external APIs, etc. These services are often technical and are not directly related to business logic.
For example, let’s define an EmailService to send order confirmation emails:
class EmailService {
sendOrderConfirmation(customer: Customer, order: Order): void {
console.log(`Sending order confirmation to ${customer.name} for order ${order.orderId}`);
// Implementation for sending email...
}
}In this case:
EmailServiceis an infrastructure service that handles the technical concern of sending an email.- It can be called from within the application service or another layer to notify customers of a successful order placement.
Why Use Services in DDD?
-
Encapsulation of Logic: Services provide a way to encapsulate logic that spans multiple entities or aggregates, keeping entities clean and focused on modeling the core domain.
-
Separation of Concerns: By using services, you ensure that domain objects (entities and value objects) focus on representing data and behavior specific to the object, while services encapsulate cross-cutting logic or external interactions.
-
Reuse of Logic: Services promote reusability. For example, a
DiscountServicecan be reused in multiple parts of the system, ensuring consistent business logic for discounts. -
Simplified Entities: Keeping complex logic in services rather than in domain entities reduces the complexity of entities, making them easier to maintain and test.
-
Testing: Services are easy to test because they usually contain isolated logic. They don’t hold internal state, and you can mock their dependencies (e.g., repositories or other services) in tests.
Guidelines for Identifying Services
-
Logic that crosses multiple aggregates: If a business operation spans multiple entities or aggregates (e.g., transferring money between bank accounts), consider using a service.
-
Logic that doesn’t fit a single entity: When a piece of logic doesn’t naturally belong to a single entity, move it to a service.
-
Technical concerns: Tasks like sending emails, logging, or interacting with external systems are handled by infrastructure services.
Example of Testing a Service in TypeScript
Since services are stateless and don’t maintain internal state, they are easy to unit test. Here’s an example of testing the DiscountService:
describe('DiscountService', () => {
it('should apply a 10% discount for loyal customers', () => {
const customer = new Customer('customer123', 'Alice');
customer.setLoyal(true); // Assume the customer is loyal
const order = new Order('order123', 'customer123', new Date());
order.addItem(new OrderItem('product1', 2, 50)); // Total amount: $100
const discountService = new DiscountService();
const discount = discountService.calculateDiscount(order, customer);
expect(discount).toBe(10); // 10% of $100
});
it('should apply a 5% discount for orders over $500', () => {
const customer = new Customer('customer123', 'Bob');
const order = new Order('order456', 'customer123', new Date());
order.addItem(new OrderItem('product1', 10, 60)); // Total amount: $600
const discountService = new DiscountService();
const discount = discountService.calculateDiscount(order, customer);
expect(discount).toBe(30); // 5% of $600
});
});Conclusion
In Domain-Driven Design, services are used to encapsulate domain logic or operations that don’t fit naturally within entities or value objects. They are crucial for:
- Handling cross-cutting logic.
- Ensuring clean separation between domain logic and technical concerns.
- Promoting reusability and maintainability.
Next
Visit Politics to dive deeper into Domain Driven Design.
Check next:
READ
Latest readings
Readings are sites which will help you with detailed
information about given topic. Read latest ones from Learn.
06-03-2026
Build your own local voice assistant powered by Ollama.
06-03-2026
Generate YouTube thumbnails with FastAPI and Ollama.
05-09-2024
Compare Neo4j and Tigergraph databases, which is easier to work with, etc.