Table of Contents
- 05 — gRPC Microservice
- Overview
- Learning Objectives
- Requirements
- Architecture
- Suggested Project Layout
- Data Model / Database
- API Design
- Tech Stack
- Implementation Milestones
- Testing Strategy
- Deployment
- Documentation Deliverables
- Stretch Goals / Future Improvements
- Lessons-Learned Prompts
- Portfolio & Resume
05 — gRPC Microservice¶
Overview¶
This project builds a production-grade Orders microservice exposed over gRPC with a transcoded REST gateway. It is the capstone of the "Learn Go" track: you take everything from earlier modules (interfaces, concurrency, error handling, testing) and assemble them into a service that looks and behaves like something you would actually ship.
The service owns the Order aggregate: customers create orders, query them,
update them while they are still mutable, cancel them, and subscribe to a live
stream of order events. The implementation is contract-first (the .proto
file is the source of truth) and follows a hexagonal / ports-and-adapters
architecture so the domain core is completely decoupled from gRPC, Postgres, and
HTTP.
The emphasis is less on the business logic (orders are deliberately simple) and more on the engineering scaffolding that distinguishes a toy from a service: interceptor chains, streaming RPCs, graceful shutdown, health checks, reflection, backwards-compatible schema evolution, and a test pyramid that runs without external dependencies wherever possible.
Level: Advanced. You are expected to be comfortable with goroutines,
channels, context.Context, and Go modules before starting.
Learning Objectives¶
By the end of this project you should be able to:
- Design a contract-first API using Protocol Buffers (proto3) and generate
Go code with
buf. - Implement unary and server-streaming RPCs with
google.golang.org/grpc. - Build a reusable interceptor chain (logging, recovery, auth, metrics) on both server and client side, and explain how it differs from HTTP middleware.
- Apply hexagonal architecture: keep domain logic free of transport and persistence concerns behind explicit ports (Go interfaces) and adapters.
- Wire dependencies with constructor injection, and understand when a
compile-time DI tool like
google/wireearns its keep. - Expose the same service as REST using
grpc-gatewaywithout duplicating handlers. - Implement a Postgres adapter with
pgxthat maps cleanly between proto DTOs, domain models, and DB rows. - Add operational concerns: TLS, gRPC health checking, server reflection, and graceful shutdown.
- Evolve a schema without breaking clients and prove it with
buf breaking. - Test with bufconn in-memory listeners and testcontainers for the database — fast, hermetic, CI-friendly.
Requirements¶
Functional¶
The service implements OrderService with the following RPCs:
| RPC | Type | Description |
|---|---|---|
CreateOrder |
unary | Validate items, compute total, persist a new order in PENDING. Returns the created order with a server-assigned ID. |
GetOrder |
unary | Fetch a single order by ID. Returns NOT_FOUND if absent. |
ListOrders |
unary | Paginated list (page token + page size), optional filter by customer_id and status. |
UpdateOrder |
unary | Modify items/shipping on an order that is still PENDING. Uses a field mask. Rejects updates to terminal orders with FAILED_PRECONDITION. |
CancelOrder |
unary | Transition an order to CANCELLED. Idempotent: cancelling an already-cancelled order succeeds. Emits an event. |
WatchOrders |
server-streaming | Subscribe to a stream of OrderEvents (created/updated/cancelled/status changed), optionally filtered by customer_id. Stream ends when the client cancels its context. |
Domain rules:
- An order has one or more
OrderItems; an empty item list is invalid (INVALID_ARGUMENT). - Status lifecycle:
PENDING → CONFIRMED → SHIPPED → DELIVERED, withPENDING/CONFIRMED → CANCELLEDpermitted. Illegal transitions are rejected. total_centsis derived server-side from items; clients cannot set it.- Cancellation and status changes publish an event consumed by
WatchOrderssubscribers and an outbound event publisher port.
Non-Functional¶
- Contract-first. The
.protofiles are authoritative; Go types are generated, never hand-written. CI fails if generated code is stale. - Backwards-compatible evolution. Field numbers are never reused; fields are
added, not repurposed.
buf breakingruns against themainbranch in CI. - Cross-cutting concerns via interceptors. Logging, panic recovery, auth, request validation, and metrics are interceptors — never copy-pasted into handlers.
- TLS on the wire by default; plaintext only behind a
--insecuredev flag. - Health checks via the standard
grpc.health.v1.Healthservice. - Graceful shutdown: on
SIGTERM/SIGINT, stop accepting new RPCs, drain in-flight ones with a deadline, then close DB pools. - Observability hooks: structured logs (
slog), Prometheus metrics, and a request ID propagated through context and metadata. - Determinism in tests: no network, no Docker required for the default
go test ./...run (testcontainers gated behind a build tag / env flag).
Architecture¶
Hexagonal (ports & adapters). The application core depends only on ports (Go interfaces it declares). Adapters on the left drive the core; adapters on the right are driven by it. The interceptor chain sits in front of the gRPC handler adapter and is purely transport-level.
flowchart LR
subgraph clients[Clients]
cli[gRPC client]
web[REST / HTTP client]
end
subgraph inbound[Inbound Adapters]
direction TB
gw[grpc-gateway\nREST -> gRPC]
subgraph chain[Server Interceptor Chain]
direction TB
i1[recovery] --> i2[logging] --> i3[auth] --> i4[validate] --> i5[metrics]
end
srv[gRPC server\nOrderServiceServer]
end
subgraph core[Application Core]
direction TB
uc[Use cases\nCreateOrder / GetOrder / ...]
dom[Domain\nOrder, OrderItem, Status, rules]
subgraph ports[Ports]
rp[[OrderRepository]]
ep[[EventPublisher]]
end
uc --> dom
uc --> rp
uc --> ep
end
subgraph outbound[Outbound Adapters]
direction TB
pg[(Postgres adapter\npgx)]
pub[Event publisher\nin-mem / NATS]
end
db[(PostgreSQL)]
cli -->|HTTP/2 + protobuf| chain
web --> gw --> chain
chain --> srv
srv -->|calls| uc
rp -. implemented by .-> pg
ep -. implemented by .-> pub
pg --> db
classDef port fill:#fef3c7,stroke:#d97706;
class rp,ep port;
The dotted "implemented by" edges are the ports/adapters boundary: the core
defines OrderRepository and EventPublisher interfaces; the Postgres and
publisher adapters implement them. The core never imports pgx, grpc, or
net/http.
Suggested Project Layout¶
Follows golang-standards/project-layout.
Generated code lives under gen/ and is committed (or regenerated in CI).
05-grpc-microservice/
├── cmd/
│ └── server/
│ └── main.go # composition root: build adapters, wire core, serve
├── api/
│ └── proto/
│ └── order/
│ └── v1/
│ ├── order.proto # messages + OrderService
│ └── events.proto # OrderEvent for WatchOrders
├── gen/
│ └── order/
│ └── v1/
│ ├── order.pb.go # generated messages
│ ├── order_grpc.pb.go # generated server/client stubs
│ ├── order.pb.gw.go # generated grpc-gateway
│ └── order.pb.validate.go # generated validators (protovalidate/protoc-gen-validate)
├── internal/
│ └── order/
│ ├── domain/
│ │ ├── order.go # Order, OrderItem, OrderStatus (pure Go)
│ │ ├── errors.go # domain errors (ErrNotFound, ErrInvalidTransition)
│ │ └── order_test.go # state-machine unit tests
│ ├── app/
│ │ ├── ports.go # OrderRepository, EventPublisher interfaces
│ │ └── usecases.go # Service: CreateOrder, GetOrder, ... (depends on ports)
│ └── adapters/
│ ├── grpc/
│ │ ├── server.go # implements gen.OrderServiceServer, maps DTO<->domain
│ │ ├── mapping.go # proto <-> domain converters
│ │ └── server_test.go # bufconn integration tests
│ └── postgres/
│ ├── repository.go # implements app.OrderRepository via pgx
│ ├── mapping.go # domain <-> row converters
│ ├── migrations/ # *.up.sql / *.down.sql
│ └── repository_test.go # testcontainers
├── internal/
│ └── interceptor/
│ ├── logging.go # unary + stream logging interceptors
│ ├── recovery.go # panic -> codes.Internal
│ ├── auth.go # bearer-token metadata check
│ ├── metrics.go # prometheus counters/histograms
│ └── chain.go # helpers to assemble the chains
├── internal/
│ └── platform/
│ ├── config.go # env/flag config
│ ├── server.go # grpc.Server + gateway + health + reflection wiring
│ ├── tls.go # credentials loading
│ └── shutdown.go # signal handling + graceful drain
├── buf.yaml # buf module config + lint/breaking rules
├── buf.gen.yaml # code generation plugins
├── Makefile # generate, lint, test, build, docker targets
├── Dockerfile
├── docker-compose.yml
├── go.mod
└── README.md
Data Model / Database¶
There are three representations of an order and you map explicitly between them. Never let proto types leak into the domain or the DB layer.
1. Proto messages (transport DTO)¶
Generated into gen/order/v1. Wire-stable; uses int64 cents and
google.protobuf.Timestamp.
Order { id, customer_id, items[], status, total_cents, created_at, updated_at }OrderItem { sku, name, quantity, unit_price_cents }enum OrderStatus { UNSPECIFIED, PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }
2. Domain entities (internal/order/domain)¶
Pure Go, no protobuf imports. Encapsulate invariants.
type OrderStatus int
const (
StatusPending OrderStatus = iota + 1
StatusConfirmed
StatusShipped
StatusDelivered
StatusCancelled
)
type Money int64 // cents, avoid float
type OrderItem struct {
SKU string
Name string
Quantity int32
UnitPriceCents Money
}
type Order struct {
ID string
CustomerID string
Items []OrderItem
Status OrderStatus
CreatedAt time.Time
UpdatedAt time.Time
}
func (o *Order) Total() Money { /* sum qty * unit price */ }
func (o *Order) Cancel() error // enforces the state machine
func (o *Order) TransitionTo(s OrderStatus) error
3. Postgres schema¶
CREATE TYPE order_status AS ENUM (
'PENDING', 'CONFIRMED', 'SHIPPED', 'DELIVERED', 'CANCELLED'
);
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id TEXT NOT NULL,
status order_status NOT NULL DEFAULT 'PENDING',
total_cents BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
id BIGSERIAL PRIMARY KEY,
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
sku TEXT NOT NULL,
name TEXT NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
unit_price_cents BIGINT NOT NULL CHECK (unit_price_cents >= 0)
);
CREATE INDEX idx_orders_customer ON orders (customer_id);
CREATE INDEX idx_orders_status ON orders (status);
-- Optional: transactional outbox for reliable event publishing
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
aggregate TEXT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published BOOLEAN NOT NULL DEFAULT false
);
Mapping discipline:
adapters/grpc/mapping.goconverts*orderv1.Order⟷domain.Order.adapters/postgres/mapping.goconvertsdomain.Order⟷ row scans.total_centsis computed in the domain (Order.Total()), persisted to the DB, and serialized into the proto — clients never supply it.- Status enum values map by name (
PENDING⟷StatusPending⟷'PENDING'), keeping the three vocabularies in sync via small lookup helpers with tests.
API Design¶
syntax = "proto3";
package order.v1;
option go_package = "github.com/learn-go/05-grpc-microservice/gen/order/v1;orderv1";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/field_mask.proto";
import "buf/validate/validate.proto"; // protovalidate
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_CONFIRMED = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
ORDER_STATUS_CANCELLED = 5;
}
message OrderItem {
string sku = 1 [(buf.validate.field).string.min_len = 1];
string name = 2;
int32 quantity = 3 [(buf.validate.field).int32.gt = 0];
int64 unit_price_cents = 4 [(buf.validate.field).int64.gte = 0];
}
message Order {
string id = 1;
string customer_id = 2 [(buf.validate.field).string.min_len = 1];
repeated OrderItem items = 3 [(buf.validate.field).repeated.min_items = 1];
OrderStatus status = 4;
int64 total_cents = 5; // server-computed, output only
google.protobuf.Timestamp created_at = 6;
google.protobuf.Timestamp updated_at = 7;
}
message OrderEvent {
enum Type {
TYPE_UNSPECIFIED = 0;
TYPE_CREATED = 1;
TYPE_UPDATED = 2;
TYPE_STATUS_CHANGED = 3;
TYPE_CANCELLED = 4;
}
Type type = 1;
Order order = 2;
google.protobuf.Timestamp emitted_at = 3;
}
message CreateOrderRequest { string customer_id = 1; repeated OrderItem items = 2; }
message CreateOrderResponse { Order order = 1; }
message GetOrderRequest { string id = 1 [(buf.validate.field).string.uuid = true]; }
message GetOrderResponse { Order order = 1; }
message ListOrdersRequest {
string customer_id = 1; // optional filter
OrderStatus status = 2; // optional filter
int32 page_size = 3 [(buf.validate.field).int32.lte = 100];
string page_token = 4; // opaque cursor
}
message ListOrdersResponse {
repeated Order orders = 1;
string next_page_token = 2;
}
message UpdateOrderRequest {
string id = 1;
repeated OrderItem items = 2;
google.protobuf.FieldMask update_mask = 3;
}
message UpdateOrderResponse { Order order = 1; }
message CancelOrderRequest { string id = 1 [(buf.validate.field).string.uuid = true]; }
message CancelOrderResponse { Order order = 1; }
message WatchOrdersRequest { string customer_id = 1; } // empty = all
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse) {
option (google.api.http) = { post: "/v1/orders" body: "*" };
}
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse) {
option (google.api.http) = { get: "/v1/orders/{id}" };
}
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse) {
option (google.api.http) = { get: "/v1/orders" };
}
rpc UpdateOrder(UpdateOrderRequest) returns (UpdateOrderResponse) {
option (google.api.http) = { patch: "/v1/orders/{id}" body: "*" };
}
rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse) {
option (google.api.http) = { post: "/v1/orders/{id}:cancel" body: "*" };
}
rpc WatchOrders(WatchOrdersRequest) returns (stream OrderEvent) {
option (google.api.http) = { get: "/v1/orders:watch" };
}
}
RPC ⟶ REST mapping¶
| RPC | gRPC method | HTTP verb | REST path |
|---|---|---|---|
| CreateOrder | /order.v1.OrderService/CreateOrder |
POST | /v1/orders |
| GetOrder | /order.v1.OrderService/GetOrder |
GET | /v1/orders/{id} |
| ListOrders | /order.v1.OrderService/ListOrders |
GET | /v1/orders?page_size=... |
| UpdateOrder | /order.v1.OrderService/UpdateOrder |
PATCH | /v1/orders/{id} |
| CancelOrder | /order.v1.OrderService/CancelOrder |
POST | /v1/orders/{id}:cancel |
| WatchOrders | /order.v1.OrderService/WatchOrders |
GET | /v1/orders:watch (chunked / SSE-style) |
Map domain errors to gRPC status codes consistently: ErrNotFound → NotFound,
ErrInvalidTransition → FailedPrecondition, validation → InvalidArgument,
unexpected → Internal. grpc-gateway then translates these to the right HTTP
status codes automatically.
Tech Stack¶
| Concern | Choice |
|---|---|
| RPC framework | google.golang.org/grpc |
| IDL / serialization | Protocol Buffers, google.golang.org/protobuf |
| Codegen toolchain | buf with buf lint, buf breaking, buf generate |
| REST transcoding | github.com/grpc-ecosystem/grpc-gateway/v2 |
| Request validation | buf.build/go/protovalidate (or legacy envoyproxy/protoc-gen-validate) |
| Health checks | google.golang.org/grpc/health + grpc_health_v1 |
| Reflection | google.golang.org/grpc/reflection |
| Database driver | github.com/jackc/pgx/v5 + pgxpool |
| Migrations | golang-migrate or goose |
| Logging | log/slog (stdlib) |
| Interceptor toolkit | github.com/grpc-ecosystem/go-grpc-middleware/v2 (recovery, logging) |
| Metrics | github.com/prometheus/client_golang + go-grpc-middleware providers/prometheus |
| Testing | github.com/stretchr/testify, google.golang.org/grpc/test/bufconn |
| Integration DB | github.com/testcontainers/testcontainers-go |
| DI (optional) | github.com/google/wire for the composition root |
Default to constructor injection: each adapter and use case takes its
dependencies as parameters in a New... function. Reach for wire only when
cmd/server/main.go gets large enough that manual wiring becomes noisy — and
even then it is a code generator that produces the same constructor calls you
would write by hand.
Implementation Milestones¶
- Proto + buf: write
order.proto/events.proto, configurebuf.yaml(lint rules, breaking-change policy) andbuf.gen.yaml(go, go-grpc, grpc-gateway, validate plugins); runbuf generate. - Domain core:
domain.Orderwith the status state machine, money arithmetic, and exhaustive unit tests — no external imports. - Ports: define
OrderRepositoryandEventPublisherinterfaces ininternal/order/app. - Use cases: implement
app.Service(Create/Get/List/Update/Cancel/Watch) against the ports with table-driven tests using mock ports. - Postgres adapter:
pgxpool-backed repository, SQL migrations, domain⟷row mapping, optimistic concurrency on update. - gRPC server adapter: implement
OrderServiceServer, proto⟷domain mapping, error→status-code translation, theWatchOrdersstreaming loop. - Interceptor chain: recovery → logging → auth → validation → metrics, for both unary and stream, plus matching client interceptors.
- Gateway: mount grpc-gateway mux, run gRPC + HTTP on separate ports (or cmux), verify REST routes.
- Health + reflection + shutdown: register health service, enable reflection, implement signal-driven graceful shutdown.
- Tests: bufconn integration tests, interceptor tests, testcontainers DB
tests,
buf breakingin CI. - Docker: multi-stage build, docker-compose with Postgres, TLS certs.
Testing Strategy¶
A classic test pyramid, fast by default:
-
Domain unit tests (
domain/*_test.go) — pure functions and the status state machine. Microseconds, zero dependencies. Cover every illegal transition. -
Use-case tests (
app/usecases_test.go) — constructapp.Servicewith mocked ports (hand-written fakes ortestify/mock). Assert thatCreateOrdercomputes totals, thatCancelOrderis idempotent, that the event publisher is invoked, and that repository errors surface correctly. -
gRPC integration tests (
adapters/grpc/server_test.go) — spin up the real server over an in-memorybufconnlistener and exercise it through a generated client. No TCP, no ports, fully parallel-safe:
lis := bufconn.Listen(1024 * 1024)
srv := grpc.NewServer(grpc.ChainUnaryInterceptor(/* ... */))
orderv1.RegisterOrderServiceServer(srv, handler)
go srv.Serve(lis)
conn, _ := grpc.NewClient("passthrough:///bufnet",
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
return lis.DialContext(ctx)
}),
grpc.WithTransportCredentials(insecure.NewCredentials()))
client := orderv1.NewOrderServiceClient(conn)
Include a streaming test that drains WatchOrders and asserts the expected
OrderEvent sequence.
-
Interceptor tests — unit-test each interceptor in isolation by calling it with a stub
handler; assert it logs, recovers from panics (returnscodes.Internal), rejects missing/invalid auth tokens, and increments metrics. -
Contract tests —
buf breaking --against '.git#branch=main'in CI guarantees no source-breaking proto changes merge without intent. -
Postgres adapter tests (
adapters/postgres/repository_test.go) — gated behind an env flag / build tag, use testcontainers-go to launch a real Postgres, run migrations, and verify CRUD + concurrency behavior against the genuine driver.
Target: go test ./... runs green with no Docker; go test -tags=integration ./...
adds the container-backed suites.
Deployment¶
Multi-stage Docker (small, non-root, static-ish binary):
# build
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -o /out/server ./cmd/server
# runtime
FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/server /server
COPY certs/ /certs/
EXPOSE 50051 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]
docker-compose brings up the full local stack:
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: orders
POSTGRES_PASSWORD: dev
ports: ["5432:5432"]
orders:
build: .
depends_on: [postgres]
environment:
DATABASE_URL: postgres://postgres:dev@postgres:5432/orders?sslmode=disable
GRPC_ADDR: ":50051"
HTTP_ADDR: ":8080"
ports: ["50051:50051", "8080:8080"]
envoy: # optional: front the gRPC service / terminate TLS
image: envoyproxy/envoy:v1.31-latest
volumes: ["./deploy/envoy.yaml:/etc/envoy/envoy.yaml"]
ports: ["10000:10000"]
TLS: load server cert/key via credentials.NewServerTLSFromFile; generate
local certs with mkcert or openssl for dev. The --insecure flag swaps in
insecure.NewCredentials() for tooling like grpcurl.
Kubernetes notes:
- Use
grpc_health_probeor the native gRPC probe (grpc:field, k8s 1.24+) for readiness and liveness, wired togrpc.health.v1.Health. - Expose the REST gateway behind an Ingress; expose gRPC via a
Servicewith HTTP/2 enabled (gRPC-aware ingress / Gateway API). - Set
terminationGracePeriodSecondslonger than the in-flight RPC drain deadline so graceful shutdown actually completes.
Documentation Deliverables¶
- README — quickstart (
make generate,make run,grpcurlexamples,curlexamples against the gateway), architecture diagram, env-var reference. - Generated proto docs — HTML/Markdown reference via
protoc-gen-doc, wired as abuf.gen.yamlplugin. - ADRs (
docs/adr/): - ADR-0001: Hexagonal architecture — why ports & adapters, what the core may import.
- ADR-0002: Contract-first with buf — proto as source of truth, generation in CI, breaking-change policy.
- ADR-0003: Interceptors over handler logic — what belongs in the chain.
- Buf module docs — if published to the Buf Schema Registry,
link the module so clients can
buf generateagainst the remote schema.
Stretch Goals / Future Improvements¶
- mTLS — require and verify client certificates; per-service identity.
- Resilience — client-side retries with
grpc.WithDefaultServiceConfig, per-RPC deadlines, hedging, and a circuit breaker. - Event sourcing — model the order as a stream of events rather than mutable rows; rebuild state by replay.
- Transactional outbox — write events and state changes in one DB
transaction, relay to a broker (NATS/Kafka) for reliable
WatchOrdersdelivery across instances. - Service mesh — offload mTLS, retries, and metrics to Istio/Linkerd and compare with in-process interceptors.
- Client SDK generation — publish generated clients for Go/TypeScript/Python from the same proto, demonstrating contract-first reuse.
- Pagination hardening — signed, opaque cursors instead of offset tokens.
Lessons-Learned Prompts¶
- Where exactly is the line between your domain core and your adapters? Name one import you were tempted to add to the core but kept out, and why it mattered.
- You implemented cross-cutting concerns as gRPC interceptors. How does an interceptor differ from classic HTTP middleware, and how do unary and stream interceptors force different designs?
- Suppose you must add a
discount_centsfield and rename a status. Which changes are safe underbuf breakingand which are not? How would you stage a breaking change across clients you don't control? - You used constructor injection and (optionally)
wire. What did manual wiring make obvious that a framework might hide? When would you actually adoptwire? - The
WatchOrdersstream must end cleanly when a client disconnects. How didcontext.Contextcancellation propagate through your streaming handler, and what would leak if you ignored it? - Your bufconn tests run without a network. What classes of bugs can bufconn catch that a pure use-case unit test cannot, and what can it still miss compared to a real TCP + TLS deployment?
Portfolio & Resume¶
Resume Bullets¶
- Designed and built a contract-first gRPC microservice in Go
(
google.golang.org/grpc, Protocol Buffers,buf) exposing unary and server-streaming RPCs plus an auto-generated REST gateway via grpc-gateway, with CI-enforced backwards-compatibility checks (buf breaking). - Applied hexagonal (ports & adapters) architecture to fully decouple domain logic from transport and persistence, enabling a fast test pyramid: pure-domain unit tests, mocked-port use-case tests, and in-memory bufconn integration tests with no network dependency.
- Implemented a reusable server- and client-side interceptor chain (recovery, structured logging, auth, request validation, Prometheus metrics) plus gRPC health checking, server reflection, TLS, and signal-driven graceful shutdown for production readiness.
Interview Talking Points¶
- Ports & adapters in practice: how declaring
OrderRepository/EventPublisherinterfaces in the core (and implementing them in the Postgres and publisher adapters) keepspgxandgrpcout of the domain, and how that shaped the dependency direction. - The interceptor chain: ordering matters (recovery outermost, metrics
innermost), how stream interceptors wrap
grpc.ServerStream, and why this beats sprinkling concerns through handlers. - Testing with bufconn: exercising the real server + interceptors over an in-memory listener for fast, hermetic, parallel integration tests — and where testcontainers fills the remaining gap for the DB adapter.
- One service, two protocols: how grpc-gateway transcodes REST to gRPC from
the same proto via
google.api.httpannotations, and how error codes map consistently across both surfaces. - Proto backward compatibility: field-number stability, additive evolution,
reserved fields, and using
buf breakingto gate merges so external clients never break unintentionally.