Irbisa · Cheatsheet
gRPC & Microservices
gRPC and protobuf evolution, deadlines, sync vs async calls, outbox and sagas, retries and circuit breakers
What are gRPC and Protocol Buffers, and when would you choose gRPC over REST with JSON?
// orders/v1/orders.proto//// syntax = "proto3";// package orders.v1;// option go_package = "example.com/gen/orders/v1;ordersv1";//…What are the trade-offs between a monolith and microservices, and which would you choose for a five-person startup?
When should services talk synchronously over REST or gRPC, and when through a message broker? What does the asynchronous option cost?
What are the four kinds of gRPC calls, and when is streaming the right choice over a unary call?
// service Catalog {// rpc GetItem(GetItemRequest) returns (Item); // unary// rpc ListItems(ListRequest) returns (stream Item); // server streaming// rpc Upload(stream Item) returns (UploadSummary); // client streaming// rpc Sync(stream SyncEvent) returns (stream SyncEvent); // bidirectional// }…This handler returns a
fmt.Errorferror for a missing order. Which status code does the gRPC client get, and how should the handler report "not found"?func (s *server) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) { o, err := s.repo.Find(ctx, req.GetId()) if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("order %s: %w", req.GetId(), err) } if err != nil { return nil, err } return o, nil}How long should a
grpc.ClientConnlive (one per call or one per process), and what does opening a new connection for every call cost?type Payments struct{ client pb.PaymentsClient } // Built once in main; conn.Close is returned for shutdown.func NewPayments(target string) (*Payments, func() error, error) { conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})),…What do an API gateway, service discovery and a service mesh do, and which concerns do they take out of your Go code?
Which
.protochanges are safe once clients are in production, why must a deleted field number bereserved, and what does proto3optionaladd?// user.proto//// message UpdateUserRequest {// reserved 4; // was bool is_trial// reserved "is_trial"; // and the name cannot come back either// string id = 1;…How does a gRPC deadline differ from an HTTP client timeout, and what does the server see when the client's deadline passes?
func quote(ctx context.Context, c pb.PricingClient, sku string) (*pb.Quote, error) { // no default deadline in gRPC: without this a stuck server holds the call forever ctx, cancel := context.WithTimeout(ctx, 300*time.Millisecond) defer cancel() q, err := c.Quote(ctx, &pb.QuoteRequest{Sku: sku}) // remaining time sent as grpc-timeout if status.Code(err) == codes.DeadlineExceeded {…What is a gRPC interceptor, and why does an auth interceptor that works for every unary method never run for streaming ones?
func UnaryAuth(ctx context.Context, req any, _ *grpc.UnaryServerInfo, h grpc.UnaryHandler) (any, error) { ctx, err := authenticate(ctx) if err != nil { return nil, err } return h(ctx, req)…Find the bug: one client's request makes the orders pod restart, and every other in-flight call fails with
Unavailable.func (s *server) ListOrders(ctx context.Context, req *pb.ListOrdersRequest) (*pb.ListOrdersResponse, error) { limit := int(req.Page.Size) if limit <= 0 || limit > 100 { limit = 20 } orders, next, err := s.repo.List(ctx, req.GetCustomerId(), limit, req.Page.Token) if err != nil { return nil, status.Error(codes.Internal, "list orders failed") } return &pb.ListOrdersResponse{Orders: orders, NextPageToken: next}, nil}This client returns the first in-stock item from a server stream. No-match searches log
search failed: EOF, and goroutines pile up. What are the two bugs?func findFirst(ctx context.Context, c pb.CatalogClient, q string) (*pb.Item, error) { stream, err := c.Search(ctx, &pb.SearchRequest{Query: q}) if err != nil { return nil, err } for { item, err := stream.Recv() if err != nil { log.Printf("search failed: %v", err) return nil, err } if item.GetInStock() { return item, nil } }}How does gRPC's built-in retry work, which status codes belong in
retryableStatusCodes, and why must the method be idempotent before you enable it?// Passed with grpc.WithDefaultServiceConfig. Charge is listed only because it carries// an idempotency key; a write without one must never be retried by the transport.const svcConfig = `{ "methodConfig": [{ "name": [{"service": "payments.v1.Payments", "method": "Charge"}], "retryPolicy": {…What does a circuit breaker do, what are its three states, and when should it trip?
// github.com/sony/gobreaker/v2var pricingCB = gobreaker.NewCircuitBreaker[*pb.Quote](gobreaker.Settings{ Name: "pricing", MaxRequests: 3, // trial calls while half-open Interval: 30 * time.Second, // closed: reset the counts every 30 s Timeout: 10 * time.Second, // open -> half-open after 10 s…Find the bug: a week after a deploy, this circuit breaker keeps opening for 30 s at a time, though the dependency fails one call in a thousand.
func (b *Breaker) Do(fn func() error) error { b.mu.Lock() if b.failures >= 5 && time.Since(b.lastFail) < 30*time.Second { b.mu.Unlock() return ErrOpen } b.mu.Unlock() err := fn() b.mu.Lock() if err != nil { b.failures++ b.lastFail = time.Now() } b.mu.Unlock() return err}Ten replicas sit behind a Kubernetes
ClusterIPService. REST traffic spreads evenly, but gRPC sends about 90% of calls to one pod. Why, and how do you fix it?func dial() (*grpc.ClientConn, error) { return grpc.NewClient( // headless Service: DNS returns every pod IP, not one virtual IP "dns:///orders.prod.svc.cluster.local:8080", grpc.WithTransportCredentials(insecure.NewCredentials()), // the default policy is pick_first: one pod gets everything…A dependency's p99 jumps from 400 ms to 6 s and four services behind a shared caller fall over. Clients retry 3x with 30 s timeouts. What amplified it, and what stops it?
// One Bulkhead per dependency: at most cap(slot) calls in flight to it.type Bulkhead struct{ slot chan struct{} } func NewBulkhead(n int) *Bulkhead { return &Bulkhead{slot: make(chan struct{}, n)} } func (b *Bulkhead) Unary(ctx context.Context, method string, req, reply any,…Some orders never get their
OrderCreatedevent, and publishing before the commit instead gives events for orders that rolled back. Which pattern fixes both?func CreateOrder(ctx context.Context, tx *sql.Tx, o Order, payload []byte) error { _, err := tx.ExecContext(ctx, `INSERT INTO orders (id, total) VALUES ($1, $2)`, o.ID, o.Total) if err == nil { // same transaction: the event exists if and only if the order does _, err = tx.ExecContext(ctx, `INSERT INTO outbox (aggregate_id, type, payload) VALUES ($1, $2, $3)`, o.ID, "OrderCreated", payload)…Checkout must reserve stock, charge the card and book a courier, each in a different service. Why not a distributed transaction, and how does a saga work instead?
How do you build a distributed lock on Redis, and why is a lock without a fencing token unsafe?
func acquire(ctx context.Context, rdb *redis.Client, key string, ttl time.Duration) (string, bool, error) { token := uuid.NewString() // random: only the owner can release it ok, err := rdb.SetNX(ctx, "lock:"+key, token, ttl).Result() return token, ok, err}…