Cheat SheetsSystem DesignCommunication Patterns

Communication Patterns — Cheat Sheet

System Design · 6 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Communication Patterns
System Design6 topicsQuick revision reference
1

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.
HTTP + JSON — REST API example
// 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 verbose
2

Message 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.
Conceptual + Java — SQS message queue
// 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()));
}
3

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.
Conceptual + Java — event-driven order flow
// 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;
    }
}
4

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.
Conceptual — pub-sub vs point-to-point
// 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
5

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.
JavaScript + Java — polling vs long polling
// 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
}
6

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.
HTTP — RESTful resource design
// 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
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/system-design