Heximon Logo
Architecture

Architecture

When to reach for CQRS, Domain-Driven Design, the repository pattern, integration events, sagas, and workflows — and when a plain controller and a plain provider are still the right call.

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:

TierReachDeliveryWhen to use
In-processSame appSynchronous, awaitedDecouple features in one app — notifications, logging, side effects
DomainSame app, after a saveSynchronous, flushed on demandReact to state changes an aggregate recorded, within one bounded context
IntegrationAcross servicesAsync, queue-backedCommunicate 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:

PatternWhenTrade-offs
Reactive choreographyEach context reacts to an event and emits the next; no central coordinatorSimple; hard to add deadlines or visibility — see Integration Events
SagaYou need a deadline ("pay in 30 s or cancel") and want a process-state aggregateAdds a durable-timer table + poller; compensation driven by timeout handlers
WorkflowThe flow is a linear procedure with real sleeps (minutes, hours, days) or cross-cutting retry/timeout policy per stepAdds 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.
Copyright © 2026