Skip to content

Table of Contents

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/wire earns its keep.
  • Expose the same service as REST using grpc-gateway without duplicating handlers.
  • Implement a Postgres adapter with pgx that 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, with PENDING/CONFIRMED → CANCELLED permitted. Illegal transitions are rejected.
  • total_cents is derived server-side from items; clients cannot set it.
  • Cancellation and status changes publish an event consumed by WatchOrders subscribers and an outbound event publisher port.

Non-Functional

  • Contract-first. The .proto files 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 breaking runs against the main branch 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 --insecure dev flag.
  • Health checks via the standard grpc.health.v1.Health service.
  • 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.go converts *orderv1.Orderdomain.Order.
  • adapters/postgres/mapping.go converts domain.Order ⟷ row scans.
  • total_cents is 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 (PENDINGStatusPending'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, configure buf.yaml (lint rules, breaking-change policy) and buf.gen.yaml (go, go-grpc, grpc-gateway, validate plugins); run buf generate.
  • Domain core: domain.Order with the status state machine, money arithmetic, and exhaustive unit tests — no external imports.
  • Ports: define OrderRepository and EventPublisher interfaces in internal/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, the WatchOrders streaming 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 breaking in CI.
  • Docker: multi-stage build, docker-compose with Postgres, TLS certs.

Testing Strategy

A classic test pyramid, fast by default:

  1. Domain unit tests (domain/*_test.go) — pure functions and the status state machine. Microseconds, zero dependencies. Cover every illegal transition.

  2. Use-case tests (app/usecases_test.go) — construct app.Service with mocked ports (hand-written fakes or testify/mock). Assert that CreateOrder computes totals, that CancelOrder is idempotent, that the event publisher is invoked, and that repository errors surface correctly.

  3. gRPC integration tests (adapters/grpc/server_test.go) — spin up the real server over an in-memory bufconn listener 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.

  1. Interceptor tests — unit-test each interceptor in isolation by calling it with a stub handler; assert it logs, recovers from panics (returns codes.Internal), rejects missing/invalid auth tokens, and increments metrics.

  2. Contract testsbuf breaking --against '.git#branch=main' in CI guarantees no source-breaking proto changes merge without intent.

  3. 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_probe or the native gRPC probe (grpc: field, k8s 1.24+) for readiness and liveness, wired to grpc.health.v1.Health.
  • Expose the REST gateway behind an Ingress; expose gRPC via a Service with HTTP/2 enabled (gRPC-aware ingress / Gateway API).
  • Set terminationGracePeriodSeconds longer than the in-flight RPC drain deadline so graceful shutdown actually completes.

Documentation Deliverables

  • README — quickstart (make generate, make run, grpcurl examples, curl examples against the gateway), architecture diagram, env-var reference.
  • Generated proto docs — HTML/Markdown reference via protoc-gen-doc, wired as a buf.gen.yaml plugin.
  • 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 generate against 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 WatchOrders delivery 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

  1. 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.
  2. 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?
  3. Suppose you must add a discount_cents field and rename a status. Which changes are safe under buf breaking and which are not? How would you stage a breaking change across clients you don't control?
  4. You used constructor injection and (optionally) wire. What did manual wiring make obvious that a framework might hide? When would you actually adopt wire?
  5. The WatchOrders stream must end cleanly when a client disconnects. How did context.Context cancellation propagate through your streaming handler, and what would leak if you ignored it?
  6. 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 / EventPublisher interfaces in the core (and implementing them in the Postgres and publisher adapters) keeps pgx and grpc out 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.http annotations, and how error codes map consistently across both surfaces.
  • Proto backward compatibility: field-number stability, additive evolution, reserved fields, and using buf breaking to gate merges so external clients never break unintentionally.