// Architecture · ~12 min read

Domain-Driven Design for SaaS that survives production

A pragmatic guide to Domain-Driven Design applied to modern SaaS: bounded contexts, aggregates, domain events, hexagonal architecture, CQRS. Production lessons from Synapz.

Why DDD today

Domain-Driven Design (DDD), formalized by Eric Evans in 2003, is not a trend: it's an engineering discipline built for complex systems, where business logic far exceeds CRUD. In 2026, most B2B SaaS fall in that bucket, multi-tenant, usage-based billing, regulated workflows, third-party integrations, AI in the loop.

When we talk about "software that survives production", DDD answers three concrete questions: where should business rules live, how do we evolve them without breaking everything, and how do we keep the code readable by the team (and by our future selves in six months).

Ubiquitous language, brick zero

DDD starts before the code. It's an explicit agreement between developers and business experts about words. A "contract" in the legal back-office does not share the same lifecycle as a "contract" on the billing side. If the team hasn't drawn the line, the code will encode the confusion, and debt piles up silently.

In practice: keep a living glossary (Notion, README, whatever) with terms, definitions, and their context. That glossary shows up in module names, classes, tables, events.

Bounded contexts: slice before you architect

The classic trap of a growing SaaS: a single User model shared by auth, billing, support, analytics. After 18 months, changing that model becomes an act of courage. DDD proposes to split along business contexts, each context owns its model, its language, sometimes its own database.

For a car-listing SaaS (a real Synapz case), we typically isolate:

  • Identity & Access, accounts, organizations, roles, sessions.
  • Sourcing, listing feeds, deduplication, AI scoring.
  • Workspace, favorites, alerts, team collaboration.
  • Billing, subscriptions, quotas, invoices.

Each context exposes a minimal contract to the others: internal API, events, external IDs. That's what lets 2–3 devs move in parallel without stepping on each other.

Aggregates, entities, value objects

Inside a bounded context, you identify aggregates: clusters of objects treated as a single transactional unit. Golden rule: one aggregate = one consistency boundary. If two objects must be modified together in a single transaction, they belong in the same aggregate.

Inside them:

  • Entities, objects with a stable identity (a Subscription, a Lead).
  • Value objects, immutable objects defined by their values (Money, EmailAddress, DateRange). No ID, compared by value, extremely testable.

Concrete win: business logic lives in the objects, not scattered across anemic services.Invoice.markAsPaid() enforces the invariant;InvoiceService.markAsPaid(invoice) guarantees nothing.

Hexagonal architecture and ports/adapters

DDD pairs naturally with hexagonal architecture (aka "ports & adapters"). The domain sits at the center, isolated from the framework, the database, third-party APIs. Anything I/O goes through ports (interfaces) implemented by adapters (PostgreSQL, Stripe, OpenAI, etc.).

Practical outcome: you can test the domain without a database, swap Stripe for Paddle without touching business logic, and deploy serverless or monolithic depending on load. The stack becomes an implementation detail, not a coupling factor.

Domain events and cross-context integration

Bounded contexts communicate through domain events:OrderPlaced, SubscriptionRenewed, LeadQualified. The publisher doesn't care who listens; the subscriber reacts at its own pace.

On the Node.js / TypeScript stack Synapz runs in production: publish via a queue (SQS, Redis Streams, PostgreSQL outbox pattern) and consume on the subscriber side. The outbox pattern is crucial to guarantee atomicity between "DB write + event publish".

CQRS, when it's worth it

CQRS separates the write model from the read model. The write model protects invariants; the read model is optimized for the UI (denormalized projections, materialized views, dedicated indexes).

It's not mandatory. On a simple CRUD, it's free complexity. On a dashboard with 20 widgets aggregating 6 tables, a dedicated read model avoids catastrophic JOINs and consistency bugs.

Frequent anti-patterns

  • Anemic Domain Model, entities with only getters/setters, all logic in "services". Procedural code in an object-shaped costume.
  • God Aggregate, one aggregate containing half the domain, impossible to load without 12 joins.
  • Leaky abstractions, a repository returning ORM entities that expose lazy-loading, transactions, and the whole mess to the application layer.
  • Premature DDD, applying the entire book on a 3-screen MVP. DDD costs; it pays off over time.

When NOT to do DDD

DDD is expensive in framing and discipline. It has no place on:

  • A throwaway prototype or short PoC.
  • A pure-CRUD internal tool without complex business rules.
  • A one-developer project under 6 months.

But as soon as you have multiple business experts, multiple teams, or a horizon beyond 2 years, DDD becomes profitable fast.

Stack and tools we use

At Synapz, DDD doesn't rely on a magic framework. Our default foundation:

  • Strict TypeScript, types as the first modeling tool.
  • Node.js (Fastify or Hono) on the API, with a thin application layer above the domain.
  • PostgreSQL with a repository pattern + outbox for events; Prisma or Kysely as adapters, never as models.
  • Vitest for ultra-fast domain unit tests (no DB, no network).
  • Zod at boundaries (HTTP, jobs, events) so no invalid data ever enters the domain.

Frequently asked questions

Is DDD only for large systems?
No, but the upfront cost only pays off on domains with real business complexity (many rules, multiple experts, >2-year horizon). On a simple CRUD or a short PoC, it's overhead for nothing.
Do I need CQRS and event sourcing to do DDD?
No. Strategic DDD (bounded contexts, ubiquitous language) and tactical DDD (aggregates, value objects) are independent from CQRS and event sourcing. Introduce those only when a concrete need justifies them.
What stack do you recommend for DDD?
At Synapz: strict TypeScript, Node.js (Fastify/Hono), PostgreSQL with a repository pattern + outbox, Zod at boundaries, Vitest for domain tests. DDD doesn't depend on a framework, it's a modeling discipline.
How do I split my bounded contexts?
By business capability, not by technical entity. An EventStorming workshop with business experts reveals natural boundaries (where the vocabulary changes, where responsibilities change). Ideally one context = one autonomous team.
Is DDD the same as microservices?
No. A bounded context can live inside a modular monolith. Microservices are a deployment choice; bounded contexts are a modeling choice. Starting with a well-split monolith is almost always the right call.

Further reading

  • Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software.
  • Vaughn Vernon, Implementing Domain-Driven Design.
  • Alberto Brandolini, EventStorming, the workshop that unlocks context slicing.