TypeScript deserved a real DDD framework - so I built one — DeepSeek Blog | Neura Market
    Neura MarketNeura Market/DeepSeek
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsBlogVideosGuidesCoursesCommunityTrendingGenerate
    DeepSeekBlogTypeScript deserved a real DDD framework - so I built one
    Back to Blog
    TypeScript deserved a real DDD framework - so I built one
    typescript

    TypeScript deserved a real DDD framework - so I built one

    DOGGA Nidhal March 23, 2026
    0 views

    When I moved from Java to TypeScript a few years ago, most things got easier. The tooling was...

    When I moved from Java to TypeScript a few years ago, most things got easier. The tooling was lighter, the iteration speed was faster, and the type system - while different - was surprisingly expressive once you learned to lean into it. But when I needed Domain-Driven Design, I hit a wall. In Java, DDD frameworks are mature and plentiful. [Axon Framework](https://www.axoniq.io/framework) gives you aggregates, event sourcing, and sagas out of the box. The patterns are well-established, the tooling is battle-tested, and the ecosystem assumes you'll want to model your domain properly. In TypeScript? The options were thin. NestJS has a [CQRS module](https://docs.nestjs.com/recipes/cqrs), but it requires buying into the entire NestJS ecosystem. Its DI container, its decorators, its module system. If you're already on NestJS, that's fine. But if you're working in an existing codebase, or you just want DDD primitives without adopting an opinionated application framework, it's a non-starter. I looked at [wolkenkit](https://www.npmjs.com/package/wolkenkit), which was genuinely promising; a CQRS and event sourcing framework built for Node.js. But the project stalled. I even reached out to one of the main contributors. It wasn't an option for production use. Beyond those, the landscape was scattered: thin libraries that gave you an event store client but no aggregate abstraction, hand-rolled patterns copied between projects, or blog posts explaining the theory without shipping usable code. Coming from the Java world, where I could model aggregates, wire up event sourcing, define projections, and coordinate sagas with well-supported tooling, the gap in TypeScript felt unnecessary. The language had everything it needed: discriminated unions, mapped types, type inference, literal types. The foundations were there. Nobody had built the framework. So I built one. I called it **noDDDe**. ## The Decider pattern over OOP aggregates The first decision was the most important: no classes. Most DDD frameworks model aggregates as classes that extend a base `AggregateRoot`. You decorate methods with `@CommandHandler`, mutate state through `this.apply()`, and wire everything together with a DI container. This works, but it fights TypeScript's strengths rather than leveraging them. noDDDe uses the [Decider pattern](https://thinkbeforecoding.com/post/2021/12/17/functional-event-sourcing-decider) instead. An aggregate is defined by three things: 1. An **initial state** what it looks like before anything happens 2. **Command handlers** (decide) given a command and the current state, return events 3. **Apply handlers** (evolve) given an event and the current state, return new state ```typescript const BankAccount = defineAggregate<BankAccountDef>({ initialState: { balance: 0 }, commands: { Deposit: (command, state) => ({ name: "DepositMade", payload: { amount: command.payload.amount }, }), }, apply: { DepositMade: (payload, state) => ({ balance: state.balance + payload.amount, }), }, }); ``` No base class. No decorators. No `this`. The `defineAggregate` function is actually an identity function, it exists only so TypeScript can infer the types. Zero runtime overhead. ## Type safety that actually helps The thing I missed most from Java wasn't the frameworks themselves, it was the confidence that the wiring was correct. TypeScript can give you that confidence, but only if the framework is designed for it. In noDDDe, you declare a types bundle: ```typescript type BankAccountDef = { state: BankAccountState; commands: BankingCommand; events: BankingEvent; infrastructure: { clock: Clock }; }; ``` From that single type, TypeScript infers everything: what commands each handler receives, what events it can return, what the apply handler's payload looks like, and what infrastructure is available. If it compiles, the wiring is correct. Commands and events are built with mapped types: ```typescript type BankingCommand = DefineCommands<{ Deposit: { amount: number }; Withdraw: { amount: number }; }>; type BankingEvent = DefineEvents<{ DepositMade: { amount: number }; WithdrawalMade: { amount: number }; }>; ``` One declaration produces the discriminated union, the payload types, and the handler signatures. No enums, no manual union types, no keeping three files in sync. ## Testing without mocks Because command handlers are functions and apply handlers are pure functions, testing is trivial: ```typescript const result = await testAggregate(BankAccount) .given( { name: "AccountCreated", payload: { id: "acc-1" } }, { name: "DepositMade", payload: { amount: 1000 } }, ) .when({ name: "Withdraw", targetAggregateId: "acc-1", payload: { amount: 200 }, }) .execute(); expect(result.events[0].name).toBe("WithdrawalMade"); expect(result.state.balance).toBe(800); ``` No framework bootstrap. No DI container setup. No mocking an event bus. Given events, when command, then events and state. That's it. ## But wait, there's more noDDDe isn't just the aggregate pattern. It's the full stack of DDD/CQRS/ES primitives: - **Projections** that fold events into read-optimized views with typed query handlers - **Sagas** for cross-aggregate workflow coordination - **Two persistence strategies**: event sourcing and state stored, swappable at configuration time without changing domain code - **Unit of Work** for atomic operations - **ORM adapters** for Drizzle, Prisma, and TypeORM with real database transactions and advisory locking - **A testing toolkit** with Given-When-Then harnesses for aggregates, projections, sagas, and full domain slices The same aggregate definition works with event sourcing or state storage. Persistence is a configuration choice, not an architecture decision. ## Where it is today noDDDe is new. The packages are published on npm (`@noddde/core`, `@noddde/engine`, `@noddde/testing`, plus ORM adapters), [the documentation](https://cloud.umami.is/q/TRBqSlPjm) is comprehensive, and there are several sample projects covering different ORMs and domain patterns. The API is stabilizing, but pre-1.0. I'm looking for feedback from TypeScript developers who've built (or tried to build) event-sourced or DDD-based systems. What's missing? What's awkward? What would make this useful for your next project? **GitHub**: [github.com/dogganidhal/noddde](https://cloud.umami.is/q/UmmBmeFe9) **Docs**: [noddde.dev](https://cloud.umami.is/q/TRBqSlPjm) **npm**: `yarn add @noddde/core @noddde/engine` Open an issue, roast the API, tell me what's missing.

    Tags

    typescriptdddcqrsnode

    Comments

    More Blog

    View all
    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠ai

    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠

    Hi everyone! 👋 I’m Tara, a Senior Software Engineer and Consultant. Over the years, I've jumped...

    T
    tworrell
    Local AI Will Save Us All (The Math Says So, Trust Me)ai

    Local AI Will Save Us All (The Math Says So, Trust Me)

    Every few weeks a take goes viral in tech circles making the case for ditching cloud AI and running...

    S
    Sebastian Schürmann
    Lost in the AI Hype, I Started Smallai

    Lost in the AI Hype, I Started Small

    And it helped me get back into tech without drowning TL;DR at the end Coming back to...

    R
    Rohini Gaonkar
    Building a Replay-Tested Interactive Brokers Client in Gogo

    Building a Replay-Tested Interactive Brokers Client in Go

    I wanted an IBKR library that felt like Go and had testing I could trust. So I wrote one.

    T
    Thomas Marcelis
    Playwright in Pictures: Fully Parallel Modeplaywright

    Playwright in Pictures: Fully Parallel Mode

    Playwright’s fullyParallel mode is often treated as a simple performance switch. In practice, it...

    V
    Vitaliy Potapov
    Designing a CLI for Both Humans and Agentscli

    Designing a CLI for Both Humans and Agents

    Learn how Alpic designed its CLI for both human developers and AI agents — covering tradeoffs like polling, context windows, interactivity, and statelessness.

    J
    Julien Vallini

    Stay up to date

    Get the latest DeepSeek prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for DeepSeek and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    © 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.