Architecture
Most apps never need this section, and that's not a gap in Heximon — it's the point. A controller with a provider or two, a database table, a handful of events, is a complete, production-shipped app. Nothing here replaces that shape; it extends it for the moment your domain actually pushes back.
The signals that you've earned it
Reach for something in this section when you recognize one of these, not before:
- Multiple writers converge on one concept. Two features both mutate "an order" and you keep re-deriving the same validity rules in each place — that logic wants one home.
- An invariant spans fields, not just one. "Can't ship before payment confirms" isn't a column constraint; it's a rule about the relationship between two pieces of state.
- Consistency has to cross a service boundary. Two independently deployed services both need to react to the same fact, and a direct call would couple their deploys together.
- A process outlives a request. "Wait 30 seconds for payment, then cancel" or "sleep two days, then follow up" can't hold a request thread open — it needs a durable clock.
None of these are architectural taste. Each maps to exactly one page below, and each is adoptable on its own — you don't buy the whole section to get one of them.
Events, promoted
Events already introduced three tiers for decoupling features. Restated here because the top two are where this section actually starts:
| Tier | Reach | Delivery | When to use |
|---|---|---|---|
| In-process | Same app | Synchronous, awaited | Decouple features in one app — notifications, logging, side effects |
| Domain | Same app, after a save | Synchronous, flushed on demand | React to state changes an aggregate recorded, within one bounded context |
| Integration | Across services | Async, queue-backed | Communicate between bounded contexts or separate services |
Domain events are the reason DDD and CQRS show up together below — an aggregate records what happened, a command handler saves it and flushes the buffer. Integration events are the reason sagas and workflows can coordinate across a deploy boundary at all.
Choreography, saga, or workflow
Once state changes are events, the next question is how do several steps stay coordinated. Three answers, each buying more machinery than the last:
| Pattern | When | Trade-offs |
|---|---|---|
| Reactive choreography | Each context reacts to an event and emits the next; no central coordinator | Simple; hard to add deadlines or visibility — see Integration Events |
| Saga | You need a deadline ("pay in 30 s or cancel") and want a process-state aggregate | Adds a durable-timer table + poller; compensation driven by timeout handlers |
| Workflow | The flow is a linear procedure with real sleeps (minutes, hours, days) or cross-cutting retry/timeout policy per step | Adds step-memoization + instance tables; the run body reads like synchronous code |
Start with plain integration events and choreography. Add a saga only when a deadline enters the picture. Reach for a workflow only when the procedure itself is linear and needs to sleep past a request's lifetime.
What each page solves
Domain-Driven Design gives multi-field invariants and business rules a home
that isn't scattered across controllers — ValueObject, Entity, and AggregateRoot are plain classes
with equality, dirty tracking, and recorded domain events built in. Prerequisite: persistence
and the repository pattern to persist what you model.
CQRS gives every write and every read an explicit, individually testable
unit — a typed CommandBus/QueryBus instead of controller methods that quietly do both. It's the layer
that saves an aggregate and flushes its domain events in one place. Prerequisite: controllers
and, once state gets real invariants, DDD.
Repository pattern gives your domain a collection-like
persistence boundary — save, getById, a zero-codegen query — so handlers never touch SQL and the
storage engine stays swappable behind one abstract token. Prerequisite: Drizzle
or another persistence adapter.
Integration events carry a fact across a bounded context or a deployed service without blocking the request that produced it — a typed producer, a typed consumer, and a transactional outbox so the publish can't outrun the save. Prerequisite: events and background jobs (the queue transport integration events ride).
Sagas give a multi-step process a deadline that survives a restart — a durable timer commits atomically with the saga's state, and a timeout handler compensates if it fires. Prerequisite: DDD (the process-state aggregate) and integration events (the channel a timeout is delivered on).
Workflows give a linear procedure — charge → sleep 2 days → ship — a
way to persist each step's result and resume after a sleep, automatically, across process restarts.
Prerequisite: background jobs (the resume wakeup rides the same
dispatch substrate) — no DDD or CQRS required, a workflow's steps can be as plain as any provider method.
None of this is a rewrite
Every concept in this section ships as one package and one line in the plugins list — CqrsPlugin,
DomainEventsPlugin, IntegrationEventsPlugin, SagaPlugin, WorkflowPlugin. You adopt them per module,
not per app: one bounded context can run full DDD and CQRS while the rest of the app stays plain
controllers and providers, side by side in the same build.
Nothing you write today has to be restructured to add one of these later — the plain controller you already have keeps working; you're choosing where to add a command bus or an aggregate, not migrating off what's there.
Next steps
- Domain-Driven Design — model invariants with
ValueObject,Entity,AggregateRoot. - CQRS — split writes from reads with a typed
CommandBus/QueryBus. - Repository pattern — a collection-like persistence boundary.
- Integration events — cross-context and cross-service events, plus the transactional outbox.
- Sagas — durable deadlines and process-state aggregates.
- Workflows — linear durable procedures that sleep across restarts.
Branded IDs
Nominal entity identifiers with the zero-runtime Branded type and the DI-free Id helper — generate, from, uuid.v7, uuid.v4, $type, and end-to-end id safety.
Domain-Driven Design
Model your domain with the Entity, ValueObject, and AggregateRoot bases — value equality, dirty tracking, optimistic concurrency, and domain events.