Skip to main content

Introduction to Tactical Domain-Driven Design

Tactical Domain-Driven Design

Domain-Driven Design (DDD) is a software development approach that focuses on creating complex software by deeply connecting the implementation to an evolving model of the core business domain. While strategic DDD provides a high-level view of system architecture, tactical DDD dives deep into the implementation patterns that make domain modeling effective and maintainable.

Key Tactical DDD Patterns

1. Entities

Entities are domain objects with a distinct identity that persists over time. Unlike value objects, entities are defined by their unique identifier rather than their attributes.

class Customer {
constructor(id, name, email) {
this._id = id; // Unique identifier
this._name = name;
this._email = email;
}

// Identity is based on the unique identifier
equals(other) {
if (!(other instanceof Customer)) return false;
return this._id === other._id;
}

// Business logic can be encapsulated within the entity
changeEmail(newEmail) {
// Validate email format
if (!this.isValidEmail(newEmail)) {
throw new Error('Invalid email format');
}
this._email = newEmail;
}

isValidEmail(email) {
// Email validation logic
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}

2. Value Objects

Value objects are immutable objects defined by their attributes rather than a unique identifier. They represent descriptive aspects of the domain.

class Address {
constructor(street, city, postalCode, country) {
this._street = street;
this._city = city;
this._postalCode = postalCode;
this._country = country;
Object.freeze(this); // Ensure immutability
}

// Equality based on all attributes
equals(other) {
if (!(other instanceof Address)) return false;
return this._street === other._street &&
this._city === other._city &&
this._postalCode === other._postalCode &&
this._country === other._country;
}

// Convenience method for formatting
format() {
return `${this._street}, ${this._city} ${this._postalCode}, ${this._country}`;
}
}

3. Aggregates

Aggregates are clusters of domain objects treated as a single unit with a root entity (aggregate root) responsible for maintaining the aggregate's invariants.

class Order {
constructor(orderId) {
this._id = orderId;
this._items = [];
this._totalPrice = 0;
}

addItem(product, quantity) {
// Business rules enforced at the aggregate root
if (quantity <= 0) {
throw new Error('Quantity must be positive');
}

const item = new OrderItem(product, quantity);
this._items.push(item);
this._calculateTotalPrice();
}

removeItem(product) {
const index = this._items.findIndex(item => item.product === product);
if (index !== -1) {
this._items.splice(index, 1);
this._calculateTotalPrice();
}
}

_calculateTotalPrice() {
this._totalPrice = this._items.reduce((total, item) =>
total + (item.product.price * item.quantity), 0);
}

get totalPrice() {
return this._totalPrice;
}
}

class OrderItem {
constructor(product, quantity) {
this.product = product;
this.quantity = quantity;
}
}

4. Domain Services

Domain services contain domain logic that doesn't naturally fit within entities or value objects.

class PaymentService {
processPayment(order, paymentMethod) {
// Complex payment processing logic
if (!this._validatePaymentMethod(paymentMethod)) {
throw new Error('Invalid payment method');
}

// Perform payment processing
const paymentResult = this._executePayment(order, paymentMethod);

if (paymentResult.status === 'success') {
order.markAsPaid();
return true;
}

return false;
}

_validatePaymentMethod(paymentMethod) {
// Validate payment method details
return paymentMethod && paymentMethod.isValid();
}

_executePayment(order, paymentMethod) {
// Simulate payment processing
return {
status: 'success',
transactionId: this._generateTransactionId()
};
}

_generateTransactionId() {
return Math.random().toString(36).substr(2, 9);
}
}

5. Repositories

Repositories provide an abstraction layer for data persistence, allowing the domain model to remain independent of data access mechanisms.

class CustomerRepository {
constructor(database) {
this._database = database;
}

save(customer) {
// Validate before saving
if (!this._isValidCustomer(customer)) {
throw new Error('Invalid customer data');
}

return this._database.save({
id: customer._id,
name: customer._name,
email: customer._email
});
}

findById(customerId) {
const customerData = this._database.findById(customerId);

if (!customerData) return null;

return new Customer(
customerData.id,
customerData.name,
customerData.email
);
}

_isValidCustomer(customer) {
return customer._name && customer._email;
}
}

Tactical Domain-Driven Design

Explanation of the Tactical Domain-Driven Design Introduction Sequence Diagram

1. Developers

  • Implement the domain model using tactical patterns.

2. Domain Model

  • The representation of the domain within the software.

3. Entities

  • Objects with a unique identity that change over time.

4. Value Objects

  • Immutable objects that represent a concept without an identity.

5. Aggregates

  • Clusters of Entities and Value Objects treated as a single unit with consistency rules.

6. Domain Services

  • Operations that don’t naturally fit within Entities or Aggregates.

7. Domain Events

  • Records of significant occurrences in the domain.

8. Repositories

  • Mechanisms for persisting and retrieving Aggregates.

9. Factories

  • Objects responsible for creating complex Aggregates.

10. Specifications

  • Objects that define query criteria for Repositories.

Best Practices for Tactical DDD

  1. Keep the Domain Model Pure: Separate domain logic from infrastructure concerns.
  2. Protect Invariants: Use aggregate roots to maintain the consistency of related objects.
  3. Use Value Objects for Descriptive Attributes: Leverage immutability and equality comparisons.
  4. Implement Rich Domain Models: Add behavior to domain objects, not just data.
  5. Use Domain Services for Complex Operations: Handle logic that spans multiple aggregates.

Common Pitfalls to Avoid

  • Anemic Domain Models: Avoid creating classes that are just data containers without behavior.
  • Overcomplicating the Model: Keep the domain model focused and aligned with business requirements.
  • Tight Coupling: Maintain loose coupling between domain objects and infrastructure.

Conclusion

info

Tactical Domain-Driven Design provides powerful patterns for creating robust, maintainable software systems. By carefully designing entities, value objects, aggregates, and services, you can create a domain model that closely reflects the business domain and evolves with changing requirements.

Remember, the goal of tactical DDD is not to create complex code, but to create code that clearly expresses the business domain and makes complex business logic manageable and understandable.