April 29, 2026
9 min read
Pragmatic DDD for Web Products
Ubiquitous language, bounded contexts, and transaction boundaries without turning domain modeling into bureaucracy.
Leer en espanolDDD starts with language, not entities
Many teams jump straight into `User`, `Order`, or `Invoice` classes and assume they are doing DDD. They are usually skipping the important part: agreeing on language. What exactly is an active customer? When is an order considered closed? What is the difference between an invitation, a member, and a collaborator?
When product, support, and engineering use different words for the same workflow, the model ends up encoding ambiguity. DDD is valuable because it aligns language with the system instead of letting every layer invent its own translation.
Bounded contexts are boundaries of change
A bounded context is not an elegant folder structure. It is a place where a term has one specific meaning. “Account” may mean an organization in billing, a tenant in permissions, and a profile in onboarding. Forcing a single global definition often creates more friction than order.
In web products, contexts are often easier to identify as zones of coordinated change: billing changes because of financial rules, identity changes because of permissions and sessions, publishing changes because of editorial workflows. If everything lives in one universal model, each change becomes more expensive than it should be.
- Split a context when rules change for different reasons.
- Split when the same term is used with different intent.
- Do not split if you are only distributing tables without isolating behavior.
Keep aggregates small and meaningful
A common mistake is turning every database relationship into a large aggregate. An aggregate is not a conceptual join. It is a consistency boundary. If a rule must be enforced atomically, it may belong in the same aggregate. If not, you are probably grouping too much.
A `Workspace` may need to guarantee that there is only one primary owner. A `Subscription` may need to protect valid state transitions. But updating half a dozen collections because “it is all part of the same business process” usually leads to locking, coupling, and latency.
A small aggregate protects invariants, not tables.
class Subscription {
constructor(
readonly id: string,
private status: 'trialing' | 'active' | 'past_due' | 'canceled',
) {}
cancel() {
if (this.status === 'canceled') {
throw new Error('Subscription is already canceled');
}
this.status = 'canceled';
}
} Useful DDD feels like operational clarity
The best signal that DDD is helping is not the number of diagrams you produce. It is that important decisions live in recognizable places, names become sharper, and rules stop leaking accidentally into random endpoints and components.
If “doing DDD” forces the team into more ceremony than useful code, the process has already drifted. The goal was never to romanticize modeling. The goal was to shape a system that changes often without degrading every sprint.
More articles
Back to articlesHexagonal Architecture Without Ceremony
How to separate UI, use cases, and infrastructure in a web app without turning every change into another layer.
May 12, 2026
8 min read
Spec-Driven Development for Teams
Specify behavior before implementation to align frontend, backend, and product with less rework.
April 10, 2026
8 min read
ADRs That Keep Systems Sane
How to document technical decisions with context, tradeoffs, and consequences without writing an endless wiki.
March 21, 2026
7 min read