Day 134 — gRPC Service Skeleton¶
Month 5 · Week 4 · ⬅ Day 133 · Day 135 ➡ · Journal index
🎯 Learning Objective¶
Stand up the capstone project's skeleton: a standard Go service layout (cmd/, internal/), a wired grpc.Server with interceptors/health/reflection, and a clean lifecycle with signal-driven graceful shutdown.
📚 Topics¶
- Project layout:
cmd/<binary>/main.go,internal/,api/(proto) grpc.NewServer, service registration,net.Listener,Serve/GracefulStop- Composition root,
signal.NotifyContext, graceful shutdown
📖 Reading / Sources¶
- Go project layout (Organizing a Go module)
- gRPC-Go basics tutorial
-
grpc.Server— GracefulStop -
os/signal.NotifyContext
📝 Notes¶
- The capstone is one service: a gRPC API that accepts work and pushes it to a Redis-backed queue, with workers, retries, metrics, and docs. Today is only the skeleton → [[project-layout]].
- Layout that scales:
cmd/server/main.gois the onlymain(the composition root); all real code lives underinternal/so nothing else can import it.internal/transport/grpcholds handlers,internal/corethe domain,internal/adapter/...the infrastructure. The generated proto goes inapi/orgen/→ [[internal-package]] [[composition-root]]. - A gRPC handler must satisfy the generated server interface; embed
pb.UnimplementedXServerso adding new RPCs to the proto doesn't break compilation (forward compatibility) → [[forward-compat]]. - Lifecycle:
net.Listen→grpc.NewServer(opts...)→ register services →Serve(lis)(blocks). Shutdown must be graceful:GracefulStop()stops accepting new RPCs and waits for in-flight ones to finish, versusStop()which is immediate and abrupt → [[graceful-shutdown]]. - Drive shutdown from signals with
signal.NotifyContext: it returns a context cancelled onSIGINT/SIGTERM. RunServein a goroutine, block on<-ctx.Done(), thenGracefulStop(). This is the idiomatic replacement for the oldsignal.Notify+ channel dance → [[context]]. - Wire the cross-cutting pieces from Weeks 1-2 here: chained interceptors (logging, recovery, auth), the health service (
grpc_health_v1), and reflection for grpcurl. Health + reflection are one line each but make the service operable on day one → [[interceptors]] [[health-check]].
💻 Code Examples¶
// cmd/server/main.go — the composition root. gRPC needs third-party packages,
// so this lives as a day-note snippet, not a stdlib example.
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("listen: %v", err)
}
srv := grpc.NewServer(
grpc.ChainUnaryInterceptor( // order: first = outermost
LoggingUnary, RecoveryUnary, AuthUnary,
),
)
jobpb.RegisterJobServiceServer(srv, NewJobHandler(deps)) // our service
healthpb.RegisterHealthServer(srv, health.NewServer()) // grpc_health_v1
reflection.Register(srv) // grpcurl support
go func() {
log.Println("gRPC listening on :50051")
if err := srv.Serve(lis); err != nil {
log.Fatalf("serve: %v", err)
}
}()
<-ctx.Done() // SIGINT/SIGTERM
log.Println("shutting down…")
srv.GracefulStop() // drains in-flight RPCs, then returns
}
The graceful worker-drain mechanics behind this (channels +
WaitGroup+ context) are runnable in the stdlib example:examples/month-05/workerpool· Run:go run ./examples/month-05/workerpool
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| Graceful pool shutdown (producer closes, closer ends range) | ✅ | examples/month-05/workerpool |
🐛 Mistakes Made¶
- Called
srv.Stop()on signal → in-flight RPCs were killed mid-write. Switched toGracefulStop(). - Put domain code in a top-level package and a sibling binary imported it everywhere; moved it under
internal/to enforce the boundary.
❓ Open Questions¶
- Should
GracefulStophave a timeout fallback (forceStopafter N seconds if a stuck RPC never returns)?
🧠 Active Recall (answer without looking)¶
- Q: What is the difference between
grpc.Server.Stop()andGracefulStop()?
A
`Stop()` closes all connections immediately and cancels in-flight RPCs. `GracefulStop()` stops accepting new connections/RPCs and blocks until all in-flight RPCs complete, then returns — the safe shutdown for production.- Q: Why put service code under
internal/and keep onlymainincmd/?
A
`internal/` is enforced by the compiler: only code rooted at the parent of `internal/` can import it, so external modules (and accidental cross-imports) can't depend on your private packages. `cmd/🪶 Feynman Reflection¶
The skeleton is the building's frame before any rooms: one entrypoint (cmd/server) that listens on a port, builds a gRPC server, bolts on the cross-cutting wiring (interceptors, health, reflection), and starts serving. The other half of a good frame is knowing how to take it down — graceful shutdown waits for current work to finish so no request dies half-done.
🕳️ Knowledge Gaps¶
- Best practice for a shutdown deadline and surfacing readiness vs liveness via the health service.
✅ Summary¶
I can lay out a Go service (cmd/ + internal/), construct and wire a grpc.Server with interceptors/health/reflection, and shut it down gracefully on SIGTERM with signal.NotifyContext.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 135: split the skeleton into hexagonal layers — domain ports, adapters, and a thin transport edge.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(server): grpc skeleton with health, reflection & graceful shutdown (day 134)