Layered Architecture
The strict inward dependency rule — transport → application → domain ← infrastructure.
Ductor is organized as a set of layers with a strict inward dependency rule. Dependencies point toward the domain; nothing in the core business logic knows about Postgres, Redis, HTTP, or protobuf. This separation keeps the engine testable and its seams sharp.
The dependency rule
domain/— pure business entities, domain interfaces, and domain errors. Zero infrastructure imports: nonet/http, no generated proto, no SDKs. Its only outside dependency is a small UUID utility. Everything the domain needs from the outside world is expressed as an interface it declares (domain/interfaces/), which infrastructure implements.application/— use-case orchestration. It coordinates domain objects through injected interfaces and never importsinfrastructure/. This is where the routing pipeline and workflow coordinator live.infrastructure/— concrete implementations of the domain's interfaces: Postgres stores, Redis queues and caches, connectors, observability. It depends on the domain (to implement its contracts) and on external SDKs.transport/— the ingress/egress boundary. HTTP and gRPC handlers that translate protocol requests into application calls and back. Depends only onapplication/,domain/, and generated proto stubs.pkg/— reusable libraries with zero infrastructure imports (strategy registry, CEL engine, telemetry helpers).internal/— project-private cross-cutting utilities (config, clock, async, security, test helpers).cmd/ductor/— the composition root that wires every layer together.modules/— optional feature modules that self-register.
Why the rule matters
The point of pointing dependencies inward is testability and substitution. Because the application layer depends on interfaces the domain declares — not on pgx or rueidis — you can unit-test a use case against an in-memory fake, and you can swap a storage backend without touching business logic. The domain has no reason to change when you upgrade a database driver.
It also keeps the domain honest: a rule can't accidentally reach into Redis or emit an HTTP response, because those types aren't importable from where it lives.
Dependency enforcement
Automated dependency checks reject imports that cross these layer boundaries.
The composition root
All wiring lives in cmd/ductor/fx_*.go using
go.uber.org/fx. There are no hidden
global singletons — every dependency is constructed once and injected. If you
want to know how a thing is built, you read the fx module, not a package-level
init().
Narrow interfaces over fat ones
Even within a layer, Ductor prefers narrow contracts. The full storage surface
is a composite (Storage, and the CQRS Manager), but application services
declare a minimal local interface listing only the methods they actually use
— for example, a RoutingStorageCQRS interface next to the routing adapter.
This shrinks the mock surface for tests and makes each call site's real
dependencies obvious. New code is encouraged to use the narrow per-aggregate
interfaces rather than the broad composite.
Services and lifecycle
Long-running subsystems (the workflow coordinator, queue drainers, retention and
archival workers) implement a common Service lifecycle: Pre (fail-fast setup,
run sequentially), Run (the blocking main loop, run concurrently), and Stop
(drain in reverse registration order). A Manager orchestrates them. This gives
the process a predictable startup and a clean, ordered shutdown.
Where to go next
- The Coordinator-Worker Model, in depth — the runtime's central pattern.
- Storage Model — how the CQRS Manager and sqlc fit the infrastructure layer.
- Extension Points — how modules contribute without violating the layering.
Architecture
How Ductor is built — a layered, fx-composed Go engine with a single-writer coordinator, Postgres for durable state, and Redis for the coordinating layer.
Durability Model: Coordinator & Workers
The architectural view of Ductor's central runtime pattern — serial state ownership, parallel execution, and the durable boundary between them.