Architecture
Binaries, directory layout, and platform wiring in the template service.
Binaries
| Binary | Path | Role |
|---|---|---|
| API | cmd/api | HTTP server + outbox publisher; flags -migrate, -openapi |
| Flowstate | cmd/flowstate | Background worker (sample loop; health only) |
| domain1Listener | cmd/domain1Listener | Consumes template-domain1-queue-listener and logs each message |
| domain2Listener | cmd/domain2Listener | Consumes template-domain2-queue-listener and logs each message |
Layout
services/template/
domains/ ← public per-domain contracts
domain1/events/
domain2/events/
internal/
domains/ ← business logic (insert row + outbox event)
domain1/
domain2/
handlers/ ← per-operation types and bodies (auth not wired yet)
command/ ← idempotency + transaction
publisher/ ← drain domain outboxes to the broker
listener/ ← domain1Listener / domain2Listener consume + log
testdb/ ← this service's migrations, over common/testdb
messageBus/ ← generated fanout topology
migrations/
db/ ← sqlc generated queries
common/ ← shared with every service, see ADR 0018
logging/ ← process logger: JSON deployed, console locally
httpapi/ ← middleware, error envelope, command and pagination
contracts, API and spec construction, root + docs
response/ ← the HTTP {header, payload} envelope
envelope/ ← event wire format on the bus
swaggerui/ ← vendored Swagger UI assets
reqmeta/ health/ testdb/What is left in services/template/ is what only this service can answer: its
domains, its schema, its outbox, its topology. The platform mechanics are in
common/, so template and accounting cannot drift apart on them — see
ADR 0018, and
ADR 0019 for the part of
handlers/ that moved with them.
Request path
Diagram source
flowchart LR
Client --> Chi
Chi --> Huma
Huma --> Command
Command --> Domain
Domain --> SQL
Command --> Outbox
Outbox --> Publisher- Handlers call
command.Domain1orcommand.Domain2. - Domain packages (
internal/domains/domain1) insert intodomain1and append todomain1_events. - The publisher drains each domain outbox to Azure Service Bus (emulator locally).
Logging
Middleware order is RequestID → tracing → RequestLog → Recover → request metadata.
The API starts the optional exporter in serving mode, and the listener runners start it
for message processing. The outbox and shared Service Bus transport preserve trace
context. See HTTP and messaging tracing.
RequestLog wraps Recover so a panic has already become a 500 by the time the
request is logged.
Each finished request produces one line — method, path, route, status,
durationMs, requestId, and command when the request matched an operation — and
the response carries X-Request-Id and X-Response-Time-Ms. Health probes and the
Swagger UI assets are skipped. serviceDomain narrows to the matched domain, so a
POST /v1/domain1 logs as template-domain1.
Format follows APP_ENV: local gets the console handler, anything else gets JSON.
Both go to stderr, because cmd/api -openapi owns stdout.
[16:57:09.049] INFO (template-domain1): POST /v1/domain1 201 (12.4 ms)
serviceDomain: "template-domain1"
env: "local"
startupPath: "services/template/cmd/api"
requestId: "0d9b1f1e-6c2f-47ab-9d0a-2f1c0f5e9b31"
status: 201
durationMs: 12.4
command: "domain1.create"requestId is written into the outbox row's headers inside the producing
transaction, published as the broker correlation id, and logged again by the
listeners — so one search covers the request and every event it caused. See
ADR 0017.
Domains
| Domain | Table | Outbox | Routes |
|---|---|---|---|
domain1 | domain1 | domain1_events | POST/GET /v1/domain1, GET /v1/domain1/{id} |
domain2 | domain2 | domain2_events | POST/GET /v1/domain2, GET /v1/domain2/{id} |
Event payloads: services/template/domains/domain1/events/ and services/template/domains/domain2/events/.
Business logic: services/template/internal/domains/domain1/create.go.
Each sample domain has its own fanout and a list of subscribers in services/template/.azure/bus/topology.yaml. See Operations.
Tests
go test ./services/template/...