Understanding the Frontend DDD Framework Remesh
What is DDD
Domain-Driven Design (DDD) focuses on organizing software architecture around business domains. The "domain" represents a specific business area described through collaboration between various stakeholders including product managers, system architects, and dveelopers. Domain experts use a shared language called the "ubiquitous language" to communicate effectively, allowing all team members to discuss problems using common terminology.
MVC vs DDD
Traditional backend MVC architecture often uses an "anemic model" where behavior and state are separated into different objects - typically POJOs (Plain Old Java Objects) and services. This approach reduces cognitive load for simple business logic and provides high efficiency for straightforward operations. However, this separation violates object-oriented principles by splitting an object's state and behavior, leading to procedural development patterns.
As business complexity increases, service logic becomes difficult to manage, potentially creating ripple effects where changes in one area impact the entire system.
DDD introduces the "rich model" concept that avoids these issues. Instead of separate POJOs and services, DDD uses domain entities with high cohesion. Domain state can only be modified through domain actions, preventing external direct modifications and containing business risks within the domain boundaries.
Advantages of DDD
- Business level: Universal language based on domain knowledge enables rapid delivery and minimal collaboration overhead
- Architecture level: Facilitates structural organization and resource focus
- Code level: Manages complexity effectively
Disadvantages of DDD
Primarily tactical challenges:
- High cognitive burden and significant costs for decomposing existing business logic
- Requires skilled teams and lacks tactical best practices
- Lower efficiency for simple business scenarios due to lack of out-of-the-box frameworks
Frontend Considerations
DDD has gained traction in backend development, with resources like Microsoft's "Tackle Business Complexity in a Microservice with DDD and CQRS Patterns". The strategic benefits of DDD also apply to frontend development.
Consider DDD when:
- Complex business domains exist (such as products, orders, fulfillment)
- Code complexity grows proportionally with business complexity
- Business logic reuse across multiple platforms is needed
DDD Example Implementation
Consider a product with several states: creation, editing, publishing, approval, and retraction. Traditional implementation might look like this:
interface ProductData {
name: string;
description: string;
}
class ProductEntity {
private draftStatus: boolean;
private approvalStatus: boolean;
private publishedStatus: boolean;
private productInfo: ProductData;
constructor(data: ProductData) {
this.draftStatus = true;
this.approvalStatus = false;
this.publishedStatus = false;
this.productInfo = data;
}
updateProduct(data: ProductData) {
if (!this.draftStatus) {
throw new Error('Only draft products can be edited');
}
this.productInfo = data;
}
submitForApproval() {
if (!this.draftStatus) {
throw new Error('Only draft products can be submitted');
}
this.approvalStatus = true;
this.draftStatus = false;
}
processApproval(success: boolean) {
if (!this.approvalStatus) {
throw new Error('Only pending products can be approved');
}
if (!success) {
this.draftStatus = true;
}
this.publishedStatus = success;
this.approvalStatus = false;
}
withdraw() {
this.approvalStatus = false;
this.draftStatus = true;
}
}
The DDD approach transforms this by creating separate entities for each state:
// Draft product state
class DraftProduct {
private content: ProductData;
constructor(data: ProductData) {
this.content = data;
}
modify(data: ProductData) {
this.content = data;
}
requestPublication(): PendingProduct {
return new PendingProduct(this.content);
}
}
// Pending approval product state
class PendingProduct {
private content: ProductData;
constructor(data: ProductData) {
this.content = data;
}
handleReview(approved: boolean): ProductState {
if (approved) {
return new PublishedProduct(this.content);
} else {
return new DraftProduct(this.content);
}
}
cancelRequest(): DraftProduct {
return new DraftProduct(this.content);
}
}
// Published product state
class PublishedProduct {
private content: ProductData;
constructor(data: ProductData) {
this.content = data;
}
retrieveInfo(): ProductData {
return this.content;
}
}
This transformation changes "one entity with multiple states" into "multiple entities with single states", focusing code on business logic for each specific state.
Remesh Framework
Remesh implements DDD concepts in frontend applications using the CQRS pattern, allowing developers to focus solely on business logic while the framework handles other concerns.
CQRS Pattern
Command Query Responsibility Segregation separates read and write operations. Commands modify entity data (create, delete, update), while queries retrieve data without modifying it.
CQRS provides significant performance benefits but introduces challenges like ensuring data synchronization and managing dual models.
Core Concepts
Domain: Business logic containers similar to components
- State: Domain state management
- Query: Data retrieval from states
- Command: State modification operations
- Event: Domain-related events
- Effect: Side effects for query/command execution
Source Code Analysis
Remesh utilizes RxJS for event distribution and data flow, abstracting data operations and leveraging RxJS capabilities for updates.
// Domain definition
export const ProductDomain = Remesh.domain({
name: 'ProductDomain',
impl: (domain) => {
// Current product state
const CurrentProductState = domain.state({
name: 'CurrentProductState',
default: {
type: 'DraftProduct',
data: null
}
});
// Product query
const ProductQuery = domain.query({
name: 'ProductQuery',
impl: ({ get }) => {
return get(CurrentProductState());
}
});
// Update command
const UpdateCommand = domain.command({
name: 'UpdateCommand',
impl: (_, newData: any) => {
const current = newData;
return CurrentProductState().new({
type: 'DraftProduct',
data: current
});
}
});
// Submit command
const SubmitCommand = domain.command({
name: 'SubmitCommand',
impl: ({ get }) => {
const currentState = get(CurrentProductState());
return CurrentProductState().new({
...currentState,
type: 'PendingProduct'
});
}
});
return {
query: { ProductQuery },
command: { UpdateCommand, SubmitCommand }
};
}
});
The framwork creates domain storage through context initialization, enabling chainable operations and standardized domain construction.
const initializeDomainStorage = (domainAction) => {
const domainContext = {
state: (options) => RemeshState(options),
query: (options) => RemeshQuery(options),
event: (options) => RemeshEvent(options),
command: (options) => RemeshCommand(options),
effect: (effectHandler) => {
if (!currentStorage.activated) {
currentStorage.effects.push(effectHandler);
}
}
};
const domainImplementation = domainAction.Domain.impl(domainContext, domainAction.args);
const currentStorage = {
id: generateId(),
Domain: domainAction.Domain,
get domain() { return domainImplementation; },
args: domainAction.args,
context: domainContext,
action: domainAction,
effects: [],
activated: false
};
return currentStorage;
};
Framework integration varies by platform - React uses Context API while Vue employs Provide/Inject mechanisms.