After years of building monolithic backends and gradually splitting them into microservices, I've developed a set of patterns that work well in production. This post covers the architecture decisions, gRPC design, and operational lessons from running microservices on AWS.
Why gRPC Over REST for Internal Services
When services talk to each other internally, REST's text-based overhead becomes wasteful. gRPC gives you:
Binary protocol via Protocol Buffers — smaller payloads, faster serialization
Strong typing from generated clients
Bidirectional streaming for real-time use cases
Built-in deadlines and cancellation
Service Boundary Principles
The hardest part of microservices isn't the technology — it's defining the right boundaries.
syntax = "proto3";service PaymentService {
rpc ProcessPayment (PaymentRequest) returns (PaymentResponse);
rpc GetPaymentStatus (StatusRequest) returns (PaymentStatus);
rpc StreamPaymentEvents (EventFilter) returns (stream PaymentEvent);
}
Error Handling with gRPC Status Codes
gRPC has rich status codes. Use them semantically.
import { status } from '@grpc/grpc-js';function handlePaymentError(err: Error): never {
if (err instanceof ValidationError) {
throw { code: status.INVALID_ARGUMENT, message: err.message };
}
if (err instanceof NotFoundError) {
throw { code: status.NOT_FOUND, message: err.message };
}
logger.error(err);
throw { code: status.INTERNAL, message: 'Internal server error' };
}
Production Lessons
The biggest surprise was how much time goes into observability, not code. Every gRPC call needs distributed tracing, structured logs with correlation IDs, and SLO-based alerting.
