Under the Hood: How I Structure Real DDD Code in TypeScript

You’ve heard the terms — domains, commands, events, repositories. But what does a real DDD-style codebase look like? Let’s break it down, file by file.

🧠 Who This Is For

This post is for junior and mid-level devs — or anyone uninitiated in Domain-Driven Design — who’ve heard about DDD and CQRS but don’t know how to organize their own codebase yet.

You can read the theory all day — but unless you’ve seen a system in the wild, it’s hard to know where to start.

So I’m walking you through mine: github.com/geggleto/cursor-typescript-rules

🧱 The Core Structure

This is a simplified example, but this layout reflects:

Explicit domains (quest, auth, points, etc.)

Command + event separation

Handler boundaries

Infrastructure decoupled from business logic

🧩 What Goes Where

/domains/<thing>/commands/

  • Pure DTOs (Data Transfer Objects)

  • Shape of the input you expect

  • No logic here — just structure

  • Think of this like a contract: this is what the command must contain to be valid for processing

/domains/<thing>/command-handlers/

  • One file per command

  • All the orchestration logic lives here

  • Calls into services, repositories, etc.

  • This is where the actual behavior of the command is defined — think of it as the entry point for business operations

/domains/<thing>/events/

  • The domain events that describe what happened

  • These are not side effects — they are facts

  • Emit these after meaningful state changes to reflect domain truth, not just to trigger reactions

/domains/<thing>/event-subscribers/

  • React to domain events

  • Side effects, like logging, notifications, integrations

  • Keeps side effects decoupled from core logic — lets your domain stay clean while still reacting to change

/domains/<thing>/services/

  • Domain logic or operations that cross multiple commands

  • Reusable business rules

  • Use services when logic doesn't neatly belong inside a single command or entity but still belongs in the domain

/domains/<thing>/<Thing>.ts

  • The root entity / aggregate (if used)

  • Models the core concept with internal consistency

  • If you need to enforce rules across multiple fields or entities within the domain, this is your go-to place

/messaging/

  • Infra layer

  • CommandBus, EventBus, ExecutionContext

  • Manages how commands and events flow, but knows nothing about domains

  • Think of this as the backbone — it wires up execution, but it doesn’t care what you're executing

/http/actions/

  • Adapters

  • Parse HTTP input

  • Turn them into commands

  • Call the bus and handle the result

  • These are the only files that know HTTP exists — they adapt the outside world to your internal system

🧼 Why This Matters

This structure:

  • Keeps business logic isolated

  • Makes responsibilities obvious

  • Allows for easy LLM-assisted codegen

  • Helps Cursor figure out where things belong

Cursor can scaffold a new feature because I have this structure.
The AI isn’t guessing — it’s following a pattern.

It also:

  • Keeps testing focused and simple

  • Makes refactoring safe

  • Speeds up onboarding for new devs

🏁 TL;DR

If you're building with TypeScript and want real-world DDD, this structure:

  • Scales with teams

  • Plays well with LLM tools

  • Keeps things clear and testable

You don’t need a framework. You just need boundaries.

Structure isn’t overhead — it’s scaffolding for velocity.

Next
Next

How Cursor Changed the Way I Build Software