ss
Navigate back to the homepage

Spaceout

beyond excelsior

Software development, application and system architecture

about meContact
Link to $https://www.facebook.com/spaceout/Link to $https://twitter.com/spaceoutLink to $https://www.instagram.com/spaceout/Link to $https://blog.spaceout.pl/Link to $https://dribbble.com/spaceoutLink to $https://behance.com/spaceoutLink to $https://github.com/massivDash/

Domain-Driven Design with TypeScript

by
Luke Celitan
category: post, reading time: 4 min

Introduction

Domain-Driven Design (DDD) is more than a set of patterns—it’s a philosophy for tackling complexity in the heart of software. Eric Evans’ seminal work, “Domain-Driven Design: Tackling Complexity in the Heart of Software,” laid the foundation for a way of thinking that puts the domain and its logic at the center of your system. In this guide, we’ll explore DDD from first principles, using TypeScript as our language of choice, and draw heavily on the technical context and best practices that have emerged from decades of real-world experience.

This is not a quick overview. Instead, it’s a comprehensive, technical deep-dive—aimed at developers and architects who want to build robust, maintainable, and scalable systems. We’ll go beyond the usual bullet points, weaving together narrative, code, and practical advice, all in a style that reflects the clarity and enthusiasm of the best technical writing.

1. Why Domain-Driven Design?

Software is at its most valuable when it solves real, complex problems for its users. But as systems grow, complexity can spiral out of control. DDD is a response to this challenge: it provides a set of priorities, practices, and patterns that help teams model the domain deeply, communicate effectively, and build systems that can evolve.

Key Principles:

  • The primary focus should be on the domain and domain logic.
  • Complex domain designs should be based on a model.
  • The model should drive both the language of the team and the structure of the code.

TypeScript and DDD: TypeScript’s type system, interfaces, and support for object-oriented and functional paradigms make it an excellent fit for DDD. We’ll use TypeScript throughout this guide to illustrate concepts and patterns.

2. Ubiquitous Language: The Foundation of DDD

A Ubiquitous Language is a shared vocabulary that is used by both developers and domain experts. It is derived from the domain model and is used in code, documentation, and conversation.

Why it matters:

  • Reduces translation errors between business and tech.
  • Makes code more readable and maintainable.
  • Surfaces ambiguities and contradictions early.

Example:

1// Instead of generic names:
2class DataManager {
3 /* ... */
4}
5
6// Use domain terms:
7class CargoBookingService {
8 /* ... */
9}

Best Practices:

  • Use domain terms in class, method, and variable names.
  • Refactor code and documentation as the language evolves.
  • Encourage domain experts to participate in model discussions.

3. Layered Architecture: Isolating the Domain

A well-structured system separates concerns into layers. The classic DDD architecture includes:

  • User Interface (Presentation) Layer
  • Application Layer
  • Domain Layer
  • Infrastructure Layer

TypeScript Example:

1// domain/cargo.ts
2export class Cargo {
3 /* ... */
4}
5
6// application/bookingService.ts
7import { Cargo } from '../domain/cargo';
8export class BookingService {
9 /* ... */
10}
11
12// infrastructure/cargoRepository.ts
13import { Cargo } from '../domain/cargo';
14export class CargoRepository {
15 /* ... */
16}

Why Layering?

  • Keeps domain logic free from technical concerns.
  • Makes testing and maintenance easier.
  • Supports scalability and flexibility.

4. Building Blocks: Entities, Value Objects, Services, Modules

Entities

Objects defined by their identity, not just their attributes.

1class Customer {
2 constructor(public readonly id: string, public name: string) {}
3 // ... business logic
4}

Value Objects

Objects defined only by their attributes, immutable, no identity.

1class Address {
2 constructor(
3 public readonly street: string,
4 public readonly city: string,
5 public readonly zip: string,
6 ) {}
7
8 equals(other: Address): boolean {
9 return (
10 this.street === other.street &&
11 this.city === other.city &&
12 this.zip === other.zip
13 );
14 }
15}

Services

Domain operations that don’t naturally fit on an Entity or Value Object.

1class OverbookingPolicy {
2 isAllowed(cargo: Cargo, voyage: Voyage): boolean {
3 return cargo.size + voyage.bookedCargoSize <= voyage.capacity * 1.1;
4 }
5}

Modules

Organize related concepts for clarity and maintainability.

1// domain/shipping/index.ts
2export * from './cargo';
3export * from './voyage';
4export * from './overbookingPolicy';

5. Aggregates, Factories, and Repositories

Aggregates

A cluster of associated objects treated as a unit for data changes. Each Aggregate has a root Entity.

1class Order {
2 private items: OrderItem[] = [];
3 // Order is the Aggregate Root
4 addItem(product: Product, quantity: number) {
5 /* ... */
6 }
7}

Aggregate Rules:

  • Only the root is referenced from outside.
  • Invariants are enforced within the Aggregate boundary.

Factories

Encapsulate complex creation logic.

1class OrderFactory {
2 static createNew(customer: Customer, items: OrderItem[]): Order {
3 // ... enforce invariants, set up relationships
4 return new Order(/* ... */);
5 }
6}

Repositories

Provide access to Aggregates, abstracting persistence.

1interface OrderRepository {
2 findById(id: string): Promise<Order | null>;
3 save(order: Order): Promise<void>;
4}

6. Supple Design: Making Models Work for You

Supple design is about making your model easy to use, extend, and reason about.

Techniques:

  • Intention-revealing interfaces
  • Side-effect-free functions
  • Assertions and invariants
  • Conceptual contours (aligning code structure with domain concepts)
  • Standalone classes (low coupling)
  • Closure of operations (operations return the same type as their input)

Example:

1class Money {
2 constructor(private amount: number, private currency: string) {}
3
4 add(other: Money): Money {
5 if (this.currency !== other.currency) throw new Error('Currency mismatch');
6 return new Money(this.amount + other.amount, this.currency);
7 }
8}

7. Strategic Design: Bounded Contexts and Context Maps

As systems grow, a single unified model becomes impractical. DDD introduces the concept of Bounded Contexts—explicit boundaries within which a particular model applies.

Context Map:

  • Visualizes the relationships between Bounded Contexts.
  • Defines integration patterns (Shared Kernel, Customer/Supplier, Conformist, Anticorruption Layer, etc.)

Example:

  • Booking Context vs. Shipping Context, each with its own model and language.

Integration Patterns:

  • Anticorruption Layer: Protects your model from external influences.
  • Shared Kernel: Shared subset of the model between teams.
  • Published Language: Agreed-upon data interchange format.

8. Distillation: Focusing on the Core Domain

Not all code is equally important. Distillation is about identifying and focusing on the Core Domain—the part of the model that gives your system its unique value.

Patterns:

  • Core Domain
  • Generic Subdomains
  • Cohesive Mechanisms
  • Segregated Core

Best Practices:

  • Assign your best developers to the Core Domain.
  • Keep supporting code generic or off-the-shelf.
  • Use documentation and code structure to highlight the Core.

9. Advanced Patterns: Specifications, Policies, and More

Specification Pattern

Encapsulates business rules as composable objects.

1interface Specification<T> {
2 isSatisfiedBy(candidate: T): boolean;
3}
4
5class OverbookingSpecification implements Specification<Booking> {
6 isSatisfiedBy(booking: Booking): boolean {
7 // ... business rule
8 }
9}

Policy/Strategy

Encapsulate algorithms or business policies.

1interface RoutingPolicy {
2 selectRoute(routes: Route[]): Route;
3}
4
5class CheapestRoutePolicy implements RoutingPolicy {
6 selectRoute(routes: Route[]): Route {
7 // ... select cheapest
8 }
9}

10. Best Practices, Pitfalls, and Real-World Advice

Best Practices

  • Start with the domain, not the technology.
  • Collaborate closely with domain experts.
  • Let the model drive the code and the language.
  • Refactor relentlessly as understanding deepens.
  • Isolate the domain from infrastructure and UI.
  • Use TypeScript’s type system to enforce invariants and clarify intent.

Common Pitfalls

  • Anemic domain models (all data, no behavior).
  • Overengineering (too many patterns, not enough value).
  • Ignoring the Ubiquitous Language.
  • Letting technical concerns pollute the domain layer.
  • Failing to define clear Bounded Contexts.

Real-World Advice

  • DDD is a journey, not a destination. Expect to iterate.
  • Not every part of your system needs deep modeling—focus on the Core.
  • Use tests to document and enforce business rules.
  • Be pragmatic: sometimes a “Smart UI” is enough for simple cases, but know when to invest in a rich domain model.

11. Conclusion and Further Reading

Domain-Driven Design is a powerful approach for building complex, business-critical systems. By focusing on the domain, cultivating a Ubiquitous Language, and structuring your code around the model, you can build software that is robust, maintainable, and a true asset to your organization.

Recommended Reading:

  • “Domain-Driven Design: Tackling Complexity in the Heart of Software” by Eric Evans
  • “Implementing Domain-Driven Design” by Vaughn Vernon
  • “Patterns, Principles, and Practices of Domain-Driven Design” by Scott Millett & Nick Tune

TypeScript Resources:

  • TypeScript Handbook
  • Advanced TypeScript Patterns and Architecture (see previous posts)

Final Thoughts

Domain-Driven Design is not a silver bullet, but it is a set of tools and a mindset that, when applied with discipline and creativity, can transform the way you build software. Use the patterns, but more importantly, use the principles. Let the domain lead, and let your code tell the story.

If you found this guide helpful, share it with your team, and let me know what deep-dive you’d like to see next.


ss

Progressive Web Apps, Offline Capabilities and Background Operations

A comprehensive, hands-on guide to building Progressive Web Apps (PWAs) with robust offline capabilities, advanced caching strategies, background sync, background fetch, push notifications, and real-world troubleshooting. Learn the architecture, implementation, performance optimization, and best practices for resilient, user-friendly web apps.

5 min czytania

Chaos Engineering

A comprehensive, hands-on guide to chaos engineering for distributed systems. Learn the principles, methodologies, tools, advanced concepts, and real-world case studies that will help you build resilient, reliable architectures.

5 min czytania

Loading search index...