Menu
Dev.to #architecture·April 2, 2026

Domain-Driven Design with Functional Deciders for Flexible Persistence

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 #architecture

The 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.

The Decider Pattern for Domain Logic

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.

  • Commands: Intents that may be invoked on an aggregate (e.g., CreateAuction, PlaceBid).
  • Events: Facts that happen during the lifecycle of an aggregate (e.g., AuctionCreated, BidPlaced).
  • State: The current projection of an aggregate, derived from applying events.
  • Decide Functions: Pure functions that take a command, current state, and infrastructure (e.g., clock) to validate rules and emit zero or more events. They enforce domain invariants.
  • Evolve Functions: Pure functions that take the current state and an event to produce the new state. They are state transformers.

Achieving Persistence Agnostic Domain Logic

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.

typescript
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.

Domain-Driven DesignDDDEvent SourcingFunctional ProgrammingTypeScriptArchitecture PatternsPersistence AgnosticCQRS

Comments

Loading comments...