Communication Patterns — Cheat Sheet
System Design · 6 topics. Download the PDF or the Instagram carousel and share it.
REST vs gRPC
REST uses HTTP/1.1 with JSON for simple, human-readable APIs. gRPC uses HTTP/2 with Protocol Buffers for high-performance, strongly-typed, streaming-capable inter-service communication.
- ✓REST: HTTP/1.1 + JSON, simple, browser-friendly, cacheable — best for public APIs.
- ✓gRPC: HTTP/2 + Protobuf, fast, type-safe, streaming — best for internal service-to-service calls.
- ✓gRPC is 2-10x faster than REST due to binary encoding, HTTP/2 multiplexing, and header compression.
- ✓gRPC supports 4 streaming modes: unary, server streaming, client streaming, bidirectional.
- ✓Common pattern: REST for external APIs, gRPC for internal microservice communication.
// REST API design
// Resource: /api/v1/orders
// Verbs: GET (read), POST (create), PUT (update), DELETE (remove)
// GET /api/v1/orders/123
// Response: 200 OK
{
"id": "123",
"userId": "u-42",
"items": [{ "productId": "p-1", "qty": 2 }],
"total": 99.99,
"status": "SHIPPED"
}
// POST /api/v1/orders
// Request body: { "userId": "u-42", "items": [...] }
// Response: 201 Created
// Pros: simple, human-readable, browser-native, cacheable
// Cons: over-fetching (get all fields), N+1 API calls,
// no streaming, text-based JSON is verboseMessage Queues
Message queues (RabbitMQ, SQS, Kafka) decouple producers and consumers, enabling asynchronous processing, load levelling, and fault-tolerant communication between services.
- ✓Message queues decouple producers and consumers — enabling async processing and load levelling.
- ✓At-least-once delivery means consumers must be idempotent (handle duplicate messages).
- ✓Dead-letter queues catch messages that fail processing repeatedly — essential for production systems.
- ✓Traditional queues (SQS, RabbitMQ) delete after consumption; Kafka retains for replay.
- ✓Use queues for task distribution; use Kafka for event streaming and fan-out to multiple consumers.
// Message queue flow
//
// Producer → [Queue] → Consumer
//
// 1. Producer sends message to queue
// 2. Queue persists message durably
// 3. Consumer pulls (or queue pushes) message
// 4. Consumer processes message
// 5. Consumer ACKs → queue deletes message
// 6. If no ACK within timeout → message redelivered (at-least-once)
// Amazon SQS example (Java SDK v2)
// Send
sqsClient.sendMessage(SendMessageRequest.builder()
.queueUrl(queueUrl)
.messageBody("{"orderId":"123","action":"process"}")
.delaySeconds(0)
.build());
// Receive + process + delete
List<Message> messages = sqsClient.receiveMessage(r -> r
.queueUrl(queueUrl)
.maxNumberOfMessages(10)
.waitTimeSeconds(20) // long polling
).messages();
for (Message msg : messages) {
processOrder(msg.body());
sqsClient.deleteMessage(r -> r.queueUrl(queueUrl).receiptHandle(msg.receiptHandle()));
}Event-Driven Architecture
Event-driven architecture (EDA) structures communication around events — immutable facts that something happened. Services produce and consume events asynchronously, enabling loose coupling, scalability, and real-time responsiveness.
- ✓Events are immutable facts ("OrderPlaced") — producers emit, consumers react independently.
- ✓EDA enables loose coupling — adding consumers requires no producer changes.
- ✓Thin events (notification) require callbacks; fat events (state transfer) make consumers self-sufficient.
- ✓Consumers must be idempotent — events may be delivered more than once.
- ✓Use the outbox pattern for reliable event publishing with database consistency.
// Request-driven (synchronous coupling)
// OrderService → PaymentService.charge() → wait → InventoryService.reserve() → wait
// Problem: if PaymentService is down, OrderService is blocked
// Event-driven (asynchronous coupling)
// OrderService emits "OrderPlaced" event → message broker
// ├→ PaymentService consumes → charges payment → emits "PaymentCompleted"
// ├→ InventoryService consumes → reserves stock
// └→ NotificationService consumes → sends confirmation email
//
// Benefits:
// - OrderService is not blocked
// - Adding EmailService = just subscribe to event (zero changes to OrderService)
// - Each service scales independently
// - Natural retry: failed consumers re-process from queue
// Spring Boot event publisher
@Service
public class OrderService {
private final KafkaTemplate<String, OrderEvent> kafka;
public Order placeOrder(OrderRequest req) {
Order order = orderRepo.save(createOrder(req));
kafka.send("order-events", order.getId(),
new OrderPlacedEvent(order.getId(), order.getUserId(), order.getTotal()));
return order;
}
}Publish-Subscribe Pattern
In the pub-sub pattern, publishers send messages to a topic (not directly to subscribers). Subscribers express interest in topics and receive all messages published to them. This decouples producers from consumers.
- ✓Pub-sub is one-to-many: one message is delivered to all subscribers of a topic.
- ✓Point-to-point queues are one-to-one: each message goes to exactly one consumer.
- ✓Kafka supports both patterns — different consumer groups for pub-sub, same group for competing consumers.
- ✓SNS + SQS fan-out is the standard AWS pattern for pub-sub with durable delivery.
- ✓Redis Pub/Sub is fast but has no persistence — messages are lost if subscribers are offline.
// Point-to-point (queue) — one consumer processes each message // Producer → [Queue] → Consumer A gets msg1 // → Consumer B gets msg2 (competing consumers) // Pub-sub (topic) — all subscribers get every message // Publisher → [Topic] → Subscriber A gets msg1 // → Subscriber B gets msg1 (both get same msg) // → Subscriber C gets msg1 // Kafka supports BOTH: // - Pub-sub: different consumer groups each get all messages // - Queue: consumers in the SAME group split messages (competing) // AWS SNS (pub-sub) + SQS (queue) combo // SNS Topic: "order-events" // ├→ SQS Queue: "payment-processing" (Payment service) // ├→ SQS Queue: "inventory-updates" (Inventory service) // └→ Lambda: "send-confirmation" (Email service) // Each subscriber gets every message independently
Long Polling, WebSockets & SSE
HTTP polling, long polling, WebSockets, and Server-Sent Events (SSE) are techniques for real-time client-server communication. Each has different trade-offs in complexity, scalability, and bidirectionality.
- ✓Short polling is simple but wasteful; long polling is more efficient but still has reconnection overhead.
- ✓WebSockets provide full-duplex, persistent connections — ideal for chat, gaming, collaboration.
- ✓SSE is simpler than WebSockets for server-to-client streaming — auto-reconnect, works with HTTP/2.
- ✓WebSockets require sticky sessions or a pub-sub backplane (Redis) for multi-server scaling.
- ✓Choose SSE for notifications/dashboards; WebSockets for bidirectional real-time features.
// Short polling — simple but wasteful
setInterval(async () => {
const res = await fetch('/api/notifications');
if (res.data.length > 0) showNotifications(res.data);
}, 5000); // every 5 seconds — 80% of requests return empty
// Long polling — server holds request until data available
async function longPoll() {
try {
const res = await fetch('/api/notifications/poll', {
signal: AbortSignal.timeout(30000) // 30s timeout
});
const data = await res.json();
showNotifications(data);
} catch (e) {
// timeout or error — reconnect
}
longPoll(); // immediately reconnect
}
longPoll();
// Server side (Spring Boot)
@GetMapping("/api/notifications/poll")
public DeferredResult<List<Notification>> poll() {
DeferredResult<List<Notification>> result = new DeferredResult<>(30000L);
notificationService.registerListener(result);
return result; // response sent when data arrives or timeout
}API Design Best Practices
Good API design uses consistent naming, proper HTTP methods, pagination, versioning, idempotency keys, and clear error responses. A well-designed API is the contract that holds distributed systems together.
- ✓Use nouns for resources, HTTP verbs for actions, and proper status codes.
- ✓Cursor-based pagination scales better than offset-based for large datasets.
- ✓Version APIs from day one — URL path versioning (/v1/) is the most common approach.
- ✓Idempotency keys prevent duplicate operations for non-idempotent methods (POST payments).
- ✓Always return structured error responses with machine-readable codes and request IDs.
// Good REST API design // Resources as nouns, HTTP verbs for actions GET /api/v1/users → 200 (list users) GET /api/v1/users/42 → 200 (get user) / 404 POST /api/v1/users → 201 (create user) PUT /api/v1/users/42 → 200 (full update) PATCH /api/v1/users/42 → 200 (partial update) DELETE /api/v1/users/42 → 204 (no content) // Nested resources for relationships GET /api/v1/users/42/orders → user's orders POST /api/v1/users/42/orders → create order for user // Filtering, sorting, pagination via query params GET /api/v1/orders?status=SHIPPED&sort=-created_at&page=2&limit=20 // HTTP status codes // 200 OK, 201 Created, 204 No Content // 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found // 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests // 500 Internal Server Error, 503 Service Unavailable