Skip to content

Table of Contents

05 — Orders gRPC Microservice

A contract-first gRPC microservice (unary + server-streaming RPCs, interceptor chain) built with hexagonal / ports-and-adapters architecture and constructor-based dependency injection.

status Go grpc

Overview

This service owns the Order aggregate. Clients create orders, fetch and list them, update items while an order is still PENDING, cancel orders (idempotently), and subscribe to a live stream of order events. See the SPEC for the full problem statement.

The design goal is to demonstrate the engineering scaffolding of a real gRPC service rather than complex business logic:

  • Contract-first — the .proto is the source of truth; Go types are generated from it.
  • Hexagonal architecture — the application core (internal/domain, internal/service) depends only on ports (Go interfaces) and never imports gRPC, protobuf, or any storage library. Adapters implement the ports.
  • Constructor injection — every component takes its dependencies as New… arguments; cmd/server/main.go is the single composition root.
  • Interceptors — logging, panic recovery, and bearer-token auth are gRPC interceptors, not handler boilerplate.
  • Operational basics — gRPC health checking, server reflection, and signal-driven graceful shutdown.

Build Prerequisites (read this first)

This module is its own Go module (module …/projects/05-grpc-microservice) and is intentionally excluded from the repo-root CI. Two steps are required before it will build:

  1. go mod tidy — resolves the gRPC + protobuf dependency graph and writes go.sum. (The committed go.mod lists only the direct requires.)
  2. make generate (buf) or make generate-protoc (protoc) — regenerates gen/order/v1/*.pb.go from the .proto.

gen/order/v1/order.pb.go and order_grpc.pb.go ship as a hand-written, generated-style STUB so the tree is browsable and the adapter layer compiles against the real symbol surface. The stub mirrors the shape of protoc output but is not wire-compatible — running codegen overwrites it with the real thing. The pure domain / service / repo layers do not import the generated package and compile + test on their own with no codegen.

Architecture

Hexagonal (ports & adapters). The application core depends only on the ports it declares (Repository, EventPublisher, EventSubscriber). Inbound adapters (the gRPC server + interceptor chain) drive the core; outbound adapters (the in-memory repo + event bus) are driven by it.

flowchart LR
    cli[gRPC client]

    subgraph inbound[Inbound Adapter]
        direction TB
        chain[Interceptor chain<br/>recovery → logging → auth]
        srv[gRPC server<br/>OrderServiceServer]
        chain --> srv
    end

    subgraph core[Application Core]
        direction TB
        uc[Service<br/>Create / Get / List / Update / Cancel / Watch]
        dom[Domain<br/>Order, OrderItem, status machine]
        rp[[Repository port]]
        ep[[EventPublisher / EventSubscriber ports]]
        uc --> dom
        uc --> rp
        uc --> ep
    end

    subgraph outbound[Outbound Adapters]
        direction TB
        mem[(In-memory repo)]
        bus[In-memory event bus]
    end

    cli -->|HTTP/2 + protobuf| chain
    srv -->|maps proto → domain| uc
    rp -. implemented by .-> mem
    ep -. implemented by .-> bus

    classDef port fill:#fef3c7,stroke:#d97706;
    class rp,ep port;

The dotted edges are the ports/adapters boundary. The core defines the interfaces; the adapters implement them. Swapping the in-memory repo for Postgres is a new adapter, not a change to the core.

Tech Stack

Go 1.22 · google.golang.org/grpc · google.golang.org/protobuf · buf / protoc · log/slog · gRPC health + reflection

Getting Started

1. Resolve dependencies

cd projects/05-grpc-microservice
make tidy          # go mod tidy  (writes go.sum)

2. Generate code from the proto

Pick one toolchain:

make generate          # buf generate   (preferred)
# or
make generate-protoc   # protoc + protoc-gen-go + protoc-gen-go-grpc

Install the tools if needed:

# buf
brew install bufbuild/buf/buf            # or see https://buf.build/docs/installation
# protoc plugins
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

3. Build / run / test

make build         # builds bin/server and bin/client
make run-server    # starts the gRPC server on :50051
make run-client    # in another shell: drives the demo
make test          # unit tests (domain + service + repo + interceptors)
make test-race     # with the race detector + coverage

The domain, service, and repo tests pass without running codegen, because those layers do not import the generated package.

4. Poke it with grpcurl

Reflection is enabled, so grpcurl can discover the service. Auth is a bearer token (AUTH_TOKEN, default dev-secret):

grpcurl -plaintext -H 'authorization: Bearer dev-secret' \
  -d '{"customer_id":"c1","items":[{"sku":"A","name":"Widget","quantity":2,"unit_price_cents":150}]}' \
  localhost:50051 order.v1.OrderService/CreateOrder

grpcurl -plaintext localhost:50051 list   # health/reflection are public

Project Layout

05-grpc-microservice/
├── api/proto/order/v1/order.proto       # authoritative contract
├── buf.yaml, buf.gen.yaml               # codegen + lint/breaking config
├── gen/order/v1/                        # GENERATED (stub committed; see notes)
│   ├── order.pb.go                      #   messages + enums
│   └── order_grpc.pb.go                 #   server/client stubs + ServiceDesc
├── cmd/
│   ├── server/main.go                   # composition root + graceful shutdown
│   └── client/main.go                   # demo driver (unary + streaming)
├── internal/
│   ├── domain/                          # entities, status machine, PORTS (no deps)
│   │   ├── order.go  errors.go  ports.go
│   │   └── order_test.go
│   ├── service/                         # use cases; depends only on ports
│   │   ├── service.go
│   │   └── service_test.go              # table-driven, fake repo + publisher
│   └── adapter/
│       ├── repo/memory.go               # in-memory Repository adapter
│       ├── eventbus/eventbus.go         # in-memory pub/sub adapter
│       └── grpc/                        # inbound gRPC adapter
│           ├── server.go  mapping.go    #   handlers + proto<->domain mapping
│           └── interceptor/             #   logging, recovery, auth
├── Makefile
└── README.md

API

service order.v1.OrderService:

RPC Type Domain error → gRPC code
CreateOrder unary invalid input → InvalidArgument
GetOrder unary missing → NotFound
ListOrders unary (paginated; filter by customer/status)
UpdateOrder unary non-PENDINGFailedPrecondition
CancelOrder unary idempotent; emits an event
WatchOrders server-streaming ends when the client cancels its context

Domain rules: an order needs ≥1 item; status lifecycle is PENDING → CONFIRMED → SHIPPED → DELIVERED with PENDING/CONFIRMED → CANCELLED; total_cents is computed server-side from the items (output-only).

Interceptor Chain

Server order is recovery → logging → auth → handler:

  • recovery (outermost) turns any panic into codes.Internal and logs the stack, so one bad request cannot crash the process.
  • logging (log/slog) emits one structured line per RPC with the final status code and latency — including calls auth rejects.
  • auth validates a bearer token from request metadata (authorization: Bearer …) with a constant-time compare, and exempts the health + reflection methods.

The client side injects the bearer token automatically via matching unary/stream client interceptors (interceptor.BearerTokenUnaryClient / …StreamClient).

Testing Strategy

A fast, dependency-free test pyramid (go test ./..., no Docker, no network):

  1. Domain unit tests — the status state machine and money arithmetic; every illegal transition is covered.
  2. Service tests — table-driven, constructed with hand-written fake repo and publisher; assert totals are computed, cancel is idempotent, events are published, and repository errors surface (wrapped with %w).
  3. Repository tests — copy semantics, duplicate detection, filtering, and offset pagination of the in-memory adapter.
  4. Interceptor tests — auth accept/reject paths, public-method bypass, and panic-to-Internal recovery, driven through stub handlers.
  5. Mapping tests — proto ⟷ domain converters, including the server-computed total and status round-trips.

The clock and ID generator are injected so service output is deterministic.

Lessons Learned

  • Where the core ends. The domain and service packages import neither grpc nor protobuf; the only place that knows both vocabularies is adapter/grpc/mapping.go. Keeping timestamppb out of the domain forced a clean anti-corruption layer.
  • Unary vs stream interceptors. A unary interceptor is a simple request→response wrap; a stream interceptor must wrap grpc.ServerStream, which is why recovery/auth need separate (but parallel) implementations.
  • Streaming cleanup. WatchOrders selects on stream.Context().Done() and defers the subscription's cancel — ignoring cancellation would leak a goroutine and a channel per disconnected client.

Future Improvements

  • Replace the in-memory repo with a pgx/Postgres adapter behind the same port (+ testcontainers integration tests).
  • Add a grpc-gateway REST transcoding layer from the same proto.
  • TLS by default (credentials.NewServerTLSFromFile) with an --insecure dev flag; mTLS as a stretch goal.
  • buf breaking in CI + Prometheus metrics interceptor.
  • bufconn end-to-end tests once real codegen runs (the stub is not wire-compatible).

Portfolio

Résumé bullets:

  • "Built a contract-first gRPC microservice in Go (google.golang.org/grpc, Protocol Buffers) exposing five unary RPCs plus a server-streaming WatchOrders event feed, with a reusable interceptor chain for recovery, structured logging, and bearer-token auth."
  • "Applied hexagonal (ports & adapters) architecture with constructor injection to keep the domain free of transport/persistence concerns, enabling a fast, dependency-free test pyramid (domain, mocked-port service, adapter, and interceptor tests)."
  • "Added production basics — gRPC health checking, server reflection, and signal-driven graceful shutdown (signal.NotifyContext + GracefulStop with a drain deadline)."

Interview talking points:

  • Ports defined in the core (Repository, EventPublisher, EventSubscriber) vs. adapters that implement them — and how that fixes the dependency direction.
  • Interceptor ordering (recovery outermost) and why stream interceptors must wrap grpc.ServerStream.
  • Contract-first codegen: proto as the source of truth, field-number stability, and additive evolution gated by buf breaking.

Projects · Repo README