Lesson 8 · CQRS — commands, queries & the read/write split · your code
Wiring without a bus
No mediator routes commands here — so how does a gRPC call find its handler? Through a plain interface field and a constructor. This is where the composition root finally shows up done right.
Your win: trace a request from controller to handler with no bus in sight, and explain why the controller declares its handler as an inline interface — then connect it back to Lesson 4's leak.
The controller holds the handler as an inline interface
With no command bus (Lesson 5), a controller needs some way to reach its handler. spike's answer: declare the handler right there as an inline interface, listing only the method this controller calls:
type EmailModifierService struct {
DB database.Ext
...
EmailCommandHandler interface { // ← an inline PORT
CreateEmail(ctx context.Context, payload *commands.CreateEmailPayload) (*dto.Email, error)
}
BulkEmailCommandHandler interface {
BulkCreateEmails(ctx context.Context, payload *commands.BulkCreateEmailsPayload) (...)
}
}
The RPC method just calls it — svc.EmailCommandHandler.CreateEmail(ctx, payload) —
and returns. No dispatch, no registry, no reflection.1
*commands.CreateEmailHandler directly. Declaring a
one-method interface instead means the controller depends only on the shape it uses — so
a unit test injects a mock handler and never touches a database. It's the same ports-and-adapters
inversion from Lesson 2, applied one layer up: the transport is an adapter, and the handler it
calls is reached through a port. Small move, big testability win.
The composition root: one constructor wires it all
So where do the real handlers and repos get created and connected? In the constructor at the transport edge — the composition root:
func NewEmailModifierService(db database.Ext, ...) *EmailModifierService {
return &EmailModifierService{
DB: db,
EmailRepo: &repositories.EmailRepo{}, // concrete adapter…
EmailCommandHandler: &commands.CreateEmailHandler{ // …injected into the handler…
DB: db,
EmailRepo: &repositories.EmailRepo{},
EmailRecipientRepo: &repositories.EmailRecipientRepo{},
},
} // …all in one place, at the edge
}
Everything concrete — the postgres adapters, the handlers — is instantiated here and
injected inward as interfaces. There's no DI framework; it's hand-written ("manual DI"). eureka does
the identical thing one level out, in cmd/server/eureka/gserver.go, following a fixed
recipe:2
This is Lesson 4, resolved
Remember conversationmgmt's leak: its core/service constructor imported
infrastructure/postgres and newed-up adapters inside the core. Now you can see
the correct pattern beside it:
| Service | Composition root | Core stays clean? |
|---|---|---|
| spike | the controller constructor (transport edge) | ✅ yes |
| eureka | cmd/server/eureka/gserver.go (outermost edge) | ✅ yes |
| conversationmgmt | inside core/service (Lesson 4 leak) | ❌ no |
Same job — connect ports to adapters — done at the edge in the healthy cases and in the core in the leaky one. "Wiring without a bus" and "where the composition root belongs" are the same question: the place that knows both the interface and the concrete type should sit as far out as possible, so everything inside it depends only on interfaces.
Combining DDD, CQRS & Clean Architecture — wiring
Three Dots Labs on assembling handlers and adapters in main with plain constructors
— the manual-DI composition root, argued for Go.
→ threedots.tech — DDD, CQRS & Clean Architecture combined
→ in-repo internal/spike/modules/email/controller/grpc/email_modifier_service.go:25-67,
internal/eureka/CLAUDE.md ("Service Wiring")
Check yourself (from memory)
Q1. With no command bus, how does a controller reach its handler?
Q2. Where are the concrete adapters and handlers instantiated?
gserver.go — news up the concretes and injects them inward as interfaces. Manual DI.
Q3. Why declare the handler as an interface, not the concrete type?
svc.EmailCommandHandler.CreateEmail(...). Interface, not concrete → the controller
mocks cleanly (ports-and-adapters, one layer up). Composition root: one
constructor at the edge news up all concretes (postgres adapters + handlers) and injects
them inward as interfaces — spike's NewEmailModifierService, eureka's
gserver.go (recipe: repo → usecase → transport → register). Manual DI, no framework.
Tie to L4: healthy services wire at the edge (core stays clean); conversationmgmt
wired inside core/service → the leak. "No bus" + "where wiring belongs" = one question:
put the composition root as far out as possible.main) instantiates the concrete repos and handlers and
injects them inward as interfaces. Controllers hold their handler as a small inline interface and
call it directly — no bus. Everything inside the composition root depends only on interfaces, so
it all mocks and tests in isolation."
1. In-repo (verified): internal/spike/modules/email/controller/grpc/email_modifier_service.go:25-30 (inline interfaces), :39-67 (wiring).
2. In-repo: internal/eureka/CLAUDE.md ("Service Wiring" — the 4-step recipe in cmd/server/eureka/gserver.go).