Getting Started with DomainDrivenJS
Domain-Driven Design (DDD) is a powerful approach to software development, but it can be challenging to implement effectively. DomainDrivenJS makes DDD more accessible by providing a composition-based toolkit that aligns with JavaScript's strengths.
What is Domain-Driven Design?
Domain-Driven Design is an approach to software development that:
- Centers on the business domain - Focusing on real-world business concepts rather than technical constructs
- Creates a shared language - Building a common vocabulary (ubiquitous language) between developers and domain experts
- Emphasizes a model-driven approach - Using models to solve complex problems within bounded contexts
- Separates strategic and tactical patterns - Providing both high-level design tools and detailed implementation patterns
Strategic DDD
Strategic DDD focuses on the big picture:
- Bounded Contexts - Defining clear boundaries where models apply
- Context Maps - Understanding relationships between different bounded contexts
- Core Domain - Identifying the most valuable part of your business
- Ubiquitous Language - Developing a shared vocabulary with domain experts
Tactical DDD
Tactical DDD provides implementation patterns:
- Value Objects - Immutable objects defined by their attributes
- Entities - Objects with identity that can change over time
- Aggregates - Clusters of objects treated as a single unit
- Domain Events - Representing significant occurrences in the domain
- Repositories - Providing collection-like interfaces for aggregates
- Services - Encapsulating domain operations that don't belong to entities
When to Use DDD
Domain-Driven Design is most valuable when:
- You're dealing with complex domains - When the business rules and processes are intricate
- Business logic is central to your application - When your application's value comes from solving domain problems well
- The application will evolve over time - When you need a model that can adapt to changing requirements
- Multiple stakeholders need to collaborate - When developers and domain experts must work closely together
DDD might be overkill for:
- Simple CRUD applications
- Temporary or throwaway projects
- Domains that are well-understood and unlikely to change
- Projects where technical complexity outweighs domain complexity
Why DomainDrivenJS?
DomainDrivenJS brings DDD to JavaScript with a modern approach:
- Composition over inheritance - Using functional factory patterns instead of deep class hierarchies
- Runtime validation with static types - Leveraging Zod for both validation and TypeScript integration
- Immutability by default - Ensuring predictable state management
- Developer experience first - Providing clear, helpful errors and minimal boilerplate
- Familiar JavaScript patterns - Working with the language rather than against it
Comparison with Other Approaches
Approach | Pros | Cons |
---|---|---|
Traditional OOP DDD | Well-documented, established patterns | Can lead to rigid inheritance hierarchies, less natural in JavaScript |
Functional Programming | Immutability, pure functions | Often requires learning new paradigms, less obvious mapping to domain concepts |
CQRS/Event Sourcing | Audit trail, temporal queries | Added complexity, eventual consistency challenges |
Anemic Domain Model | Simplicity, familiar to many developers | Business logic spread across services, harder to enforce invariants |
DomainDrivenJS | Combines best practices, natural in JS, type-safe | New library, evolving patterns |
Installation
npm
npm install domaindrivenjs
yarn
yarn add domaindrivenjs
pnpm
pnpm add domaindrivenjs
Basic Concepts
Here's a quick overview of the core building blocks in DomainDrivenJS:
import { z } from 'zod';
import { valueObject, entity, aggregate } from 'domaindrivenjs';
// 1. Value Objects - immutable objects defined by their attributes
const Money = valueObject({
name: 'Money',
schema: z.object({
amount: z.number().nonnegative(),
currency: z.string().length(3)
}),
methodsFactory: (MoneyFactory) => ({
add(other) {
if (this.currency !== other.currency) {
throw new Error('Cannot add different currencies');
}
return MoneyFactory.create({
amount: this.amount + other.amount,
currency: this.currency
});
}
})
});
// 2. Entities - objects with identity that can change over time
const Product = entity({
name: 'Product',
schema: z.object({
id: z.string().uuid(),
name: z.string().min(1),
price: Money.schema,
stockLevel: z.number().int().nonnegative()
}),
identity: 'id',
methodsFactory: (ProductFactory) => ({
decreaseStock(quantity) {
if (quantity > this.stockLevel) {
throw new Error('Not enough stock');
}
return ProductFactory.update(this, {
stockLevel: this.stockLevel - quantity
});
}
})
});
// 3. Aggregates - clusters of objects treated as a single unit
const Order = aggregate({
name: 'Order',
schema: z.object({
id: z.string().uuid(),
customerId: z.string().uuid(),
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
unitPrice: Money.schema
})),
status: z.enum(['DRAFT', 'PLACED', 'PAID', 'SHIPPED', 'COMPLETED', 'CANCELLED'])
}),
identity: 'id',
invariants: [
{
name: 'Order must have items when placed',
check: order => order.status !== 'PLACED' || order.items.length > 0
}
],
methodsFactory: (OrderFactory) => ({
addItem(product, quantity) {
// Implementation...
return OrderFactory.update(this, { /* updates */ });
},
placeOrder() {
return OrderFactory.update(this, {
status: 'PLACED'
}).emitEvent('OrderPlaced', {
orderId: this.id,
customerId: this.customerId,
timestamp: new Date()
});
}
})
});
Next Steps
Now that you understand what DDD and DomainDrivenJS are about:
- Check out the Quick Start guide to build your first domain model
- Learn more about DDD fundamentals to understand the key concepts
- Explore example applications to see DomainDrivenJS in action
Or dive straight into core concepts: