This article introduces a functional approach to Domain-Driven Design (DDD) using the Decider pattern in TypeScript, aiming to decouple domain logic from persistence concerns. It proposes a framework called noDDDe that leverages pure functions to manage domain state and events, enabling the same business logic to work seamlessly with different storage mechanisms like SQL databases and Event Stores. This approach tackles common challenges in DDD implementations, offering a more pragmatic alternative to full Event Sourcing or heavy OOP boilerplate.
Read original on Dev.to #architectureThe article critiques conventional approaches to Domain-Driven Design (DDD) in TypeScript, highlighting issues like the operational overhead of full Event Sourcing for most projects, the boilerplate associated with OOP frameworks (e.g., NestJS CQRS), and the effort required for custom in-house solutions. It proposes a functional alternative to overcome these challenges.
Instead of traditional OOP aggregates that mutate their own state, the Decider pattern treats an aggregate as a collection of pure functions. This architectural choice is crucial for achieving a complete decoupling of business logic from persistence. The domain logic, represented by these pure functions, operates solely on the current state and incoming commands to produce events, without any knowledge of how the state is stored or retrieved.
A core benefit of this functional DDD approach is the ability to easily switch persistence mechanisms. The article demonstrates wiring the same business logic to both a standard SQL database and an Event Store without modifying the domain code. This is possible because the `decide` and `evolve` functions are pure and only depend on input arguments (command, state, infrastructure) and produce outputs (events), abstracting away data loading and saving operations.
import { DefineCommands, DefineEvents } from "@noddde/core";
export type AuctionCommand = DefineCommands<{ /* ... commands ... */ }>;
export type AuctionEvent = DefineEvents<{ /* ... events ... */ }>;
export interface AuctionState { /* ... state definition ... */ }
export interface AuctionInfrastructure { clock: { now(): Date; }; }
type AuctionDef = { state: AuctionState; events: AuctionEvent; commands: AuctionCommand; infrastructure: AuctionInfrastructure; };System Design Implication
Decoupling domain logic from persistence is a crucial architectural goal for flexibility and testability. It allows system designers to evolve data storage strategies (e.g., migrating from relational to event store, or adopting CQRS patterns) with minimal impact on core business logic, reducing coupling and improving maintainability. This pattern facilitates building highly testable domain models independent of I/O concerns.