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:

internal/spike/modules/email/controller/grpc/email_modifier_service.go:25
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

Why inline interfaces, not the concrete handler The controller could hold *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:

email_modifier_service.go:39 (NewEmailModifierService, condensed)
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

eureka gserver.go — the wiring recipe 1. instantiate the postgres repo postgres.NewSubmissionRepo() 2. inject it into the usecase ctor usecase.NewAssessment(repo, ...) 3. inject the usecase into the transport grpc.NewAssessmentService(uc) 4. register the gRPC server pbv2.Register...Server(grpcServer, svc)

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:

ServiceComposition rootCore stays clean?
spikethe controller constructor (transport edge)✅ yes
eurekacmd/server/eureka/gserver.go (outermost edge)✅ yes
conversationmgmtinside 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.

Read this next

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?

The controller declares a one-method interface for the handler and calls it directly. No bus, no dispatch — just an interface field.

Q2. Where are the concrete adapters and handlers instantiated?

One composition root — spike's controller constructor, eureka's 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?

Depending on the interface (shape), not the concrete type, lets a test inject a mock handler — the Lesson 2 inversion applied at the transport layer.
Recall: wiring without a bus + the composition root.
how it connects + where + tie to L4, then reveal
No bus: the controller declares its handler as an inline interface (one method) and calls it directly — 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.
🎯 Interview one-liner "How do you wire everything without a DI framework?" → "Manual DI in one composition root at the edge — a constructor (or 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."
That's Part 2 — you can read the write side, the read side, the deliberate asymmetry, and the bus-free wiring. Take the four quizzes cold, then tell me "build Part 3" for sqlc: typed SQL, the generated querier, and the synthesis of all three patterns end to end. Ask me anything first.

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).