Skip to content

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

📝 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.go is the only main (the composition root); all real code lives under internal/ so nothing else can import it. internal/transport/grpc holds handlers, internal/core the domain, internal/adapter/... the infrastructure. The generated proto goes in api/ or gen/ → [[internal-package]] [[composition-root]].
  • A gRPC handler must satisfy the generated server interface; embed pb.UnimplementedXServer so adding new RPCs to the proto doesn't break compilation (forward compatibility) → [[forward-compat]].
  • Lifecycle: net.Listengrpc.NewServer(opts...) → register services → Serve(lis) (blocks). Shutdown must be graceful: GracefulStop() stops accepting new RPCs and waits for in-flight ones to finish, versus Stop() which is immediate and abrupt → [[graceful-shutdown]].
  • Drive shutdown from signals with signal.NotifyContext: it returns a context cancelled on SIGINT/SIGTERM. Run Serve in a goroutine, block on <-ctx.Done(), then GracefulStop(). This is the idiomatic replacement for the old signal.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 to GracefulStop().
  • 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 GracefulStop have a timeout fallback (force Stop after N seconds if a stuck RPC never returns)?

🧠 Active Recall (answer without looking)

  1. Q: What is the difference between grpc.Server.Stop() and GracefulStop()?
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.
  1. Q: Why put service code under internal/ and keep only main in cmd/?
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//main.go` is the single composition root that wires everything together.

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