After studying this material, you should be able to:
Imagine a production incident at 3am. A service is failing, but you don't know which upstream services depend on it or which downstream services it calls.
Traditional Approach:
┌─────────────────────────────────────────┐
│ Batch Processing (hourly/daily) │
│ │
│ Raw Data → Aggregate → Store Snapshot │
│ │
│ Result: Data is 1-24 hours OLD │
│ Problem: Useless during incidents │
└─────────────────────────────────────────┘
Streaming Approach:
┌─────────────────────────────────────────────┐
│ Continuous Processing │
│ │
│ Raw Data → Real-time Pipeline → Live Graph │
│ │
│ Result: Data is minutes old │
│ Benefit: Useful during incidents │
└─────────────────────────────────────────────┘
Key Insight: Freshness is not a nice-to-have. It is a fundamental requirement for incident response.
Without Backpressure:
❌ OVERFLOW
Producer ──────────────────────────► Buffer ──► Consumer
(fast) (full) (slow)
Result: Data loss, crashes, cascading failures
Think of it like a highway with traffic signals:
With Backpressure:
Stage 1 Stage 2 Stage 3
(Kafka (Resolution) (Persistence)
Consumer)
Normal:
[Produce] ──► [Process] ──► [Write DB]
fast medium fast
DB Slows Down:
[Pause] ◄── [Slow Down] ◄── [Overwhelmed]
✅ ✅ ⚠️
Signal travels BACKWARD through the pipeline
Data waits safely in Kafka
No data lost
| Scenario | Without Backpressure | With Backpressure |
|---|---|---|
| Normal load | Fast | Fast |
| Traffic spike | Data loss / crash | Slight delay |
| DB slowdown | Buffer overflow | Graceful slowdown |
| Recovery | Manual intervention | Automatic |
Key Insight: Slightly delayed real-time is vastly better than dropped data or system crashes.
In cloud environments, traffic rarely flows directly between services:
What Actually Happens:
App A ──► Load Balancer ──► App B
What Network Logs Show:
Flow 1: App A → Load Balancer
Flow 2: Load Balancer → App B
What Engineers Need:
App A → App B (the logical dependency)
Without resolving intermediaries, your topology map is cluttered with infrastructure noise instead of service relationships.
The Data Locality Problem:
Instance 1 has: [App A → Load Balancer X]
Instance 2 has: [Load Balancer X → App B]
To resolve: App A → App B
You need BOTH flows on the SAME instance
But Kafka distributes flows across instances randomly!
This is why multiple stages are necessary.
STAGE 1: Initial Aggregation
┌─────────────────────────────────────────────┐
│ Multi-Region Kafka (4 regions) │
│ ↓ │
│ Filter invalid flow logs │
│ ↓ │
│ 5-minute time-window batching │
│ ↓ │
│ Create aggregator objects │
│ ↓ │
│ Distribute via consistent hashing │
│ ↓ │
│ Stream to Stage 2 via SSE ──────────────► │
└─────────────────────────────────────────────┘
Purpose: Compress raw flows into aggregators
Reduce data volume before redistribution
STAGE 2: Intermediary Resolution
┌─────────────────────────────────────────────┐
│ Receive aggregators from Stage 1 │
│ ↓ │
│ Group by intermediary (Load Balancer, etc.) │
│ ↓ │
│ Join: (A→LB) + (LB→B) = (A→B) │
│ ↓ │
│ Redistribute resolved edges │
│ ↓ │
│ Stream to Stage 3 via SSE ──────────────► │
└─────────────────────────────────────────────┘
Purpose: Resolve infrastructure hops into
logical service-to-service edges
STAGE 3: Final Aggregation & Persistence
┌─────────────────────────────────────────────┐
│ Receive resolved aggregators │
│ ↓ │
│ Final aggregation across time windows │
│ ↓ │
│ Enrich with metadata (health, ownership) │
│ ↓ │
│ Convert to graph entities │
│ ↓ │
│ Persist to graph database (throttled) │
└─────────────────────────────────────────────┘
Purpose: Enrich and store final topology data
Two-Stage Attempt (Failed):
Stage 2 must:
- Collect ALL flows for each intermediary (data concentration)
- Resolve intermediaries (CPU intensive)
- Enrich with external data (I/O intensive)
- Persist to database (I/O intensive)
Result: Stage 2 becomes overwhelmed
Hot nodes crash
Cascading failures
Three-Stage Solution:
Stage 2: ONLY resolution + redistribution
Stage 3: ONLY enrichment + persistence
Responsibilities separated
Load spread across more instances
No single bottleneck
Not all services are equal in traffic volume:
Traffic Distribution (Power Law):
Auth Service: ████████████████████████████ 10,000 calls/sec
Recommendation: ████████████████████ 7,000 calls/sec
Typical Service: ██ 100 calls/sec
Rare Service: █ 10 calls/sec
If consistent hashing assigns Auth Service to Instance 1:
Instance 1: ████████████████████████████ OVERWHELMED
Instance 2: ██ fine
Instance 3: █ idle
Hot Instance Failure Loop:
High Traffic
↓
Memory Fills Up
↓
Garbage Collection Runs Constantly
↓
CPU Spent on GC, Not Business Logic
↓
Processing Slows Down
↓
More Memory Accumulates
↓
Instance Crashes
↓
Load Redistributes to Other Instances
↓
They Become Hot Nodes Too
↓
Cascading Failure ❌
Data Compression at Each Stage:
Raw Flow Logs: 1,000,000 records
↓
After Stage 1: 10,000 aggregators (100x compression)
↓
After Stage 2: 5,000 resolved edges (2x compression)
↓
Stage 3 writes: 5,000 graph entities
Each stage redistributes compressed data
No single instance handles raw volume
Load spreads naturally
gRPC Problems for Streaming Aggregation:
┌────────────────────────────────────────┐
│ • Serialization overhead per message │
│ • Connection pool management overhead │
│ • Memory pressure from streaming resp │
│ • More CPU on protocol than business │
│ logic │
└────────────────────────────────────────┘
SSE Advantages for This Use Case:
┌────────────────────────────────────────┐
│ • Lightweight HTTP-based protocol │
│ • Minimal serialization overhead │
│ • Natural backpressure integration │
│ • Simpler connection model │
│ • Human-readable (easier debugging) │
└────────────────────────────────────────┘
Choose gRPC when:
✅ Request-response RPC patterns
✅ Bidirectional streaming needed
✅ Strong typing across teams critical
✅ Low-latency individual calls
Choose SSE when:
✅ One-directional streaming
✅ Large volumes of pre-aggregated data
✅ Backpressure integration needed
✅ Debugging simplicity valued
Rule: Measure first. Choose based on YOUR data, not industry defaults.
Auto Scaling Groups add and remove instances dynamically. How do you route data consistently when the cluster size keeps changing?
Dynamic Consistent Hashing:
Step 1: Each instance queries service registry
Registry returns: [Instance-A, Instance-B, Instance-C]
Step 2: Sort the list (ensures all instances agree)
Sorted: [Instance-A, Instance-B, Instance-C]
Step 3: Hash the aggregator key
hash("auth-service") → maps to Instance-B
Step 4: Route aggregator to Instance-B
When Instance-D is added:
Sorted: [Instance-A, Instance-B, Instance-C, Instance-D]
Most aggregators stay on same instance
Only ~25% need to move (consistent hashing property)
No manual coordination needed ✅
Traditional Approach: Dynamic Consistent Hashing:
┌──────────────────────┐ ┌──────────────────────────┐
│ Static cluster size │ │ Dynamic cluster size │
│ Manual rebalancing │ │ Automatic rebalancing │
│ Coordination service │ │ Service registry reused │
│ Downtime during │ │ Zero downtime scaling │
│ scaling │ │ │
└──────────────────────┘ └──────────────────────────┘
Engineers need to ask: "What did the service topology look like during last Tuesday's incident?"
Mechanism 1: Time-Windowed Snapshots
┌─────────────────────────────────────────┐
│ Every 5 minutes, store aggregator state │
│ │
│ Key: (entity_id, timestamp) │
│ Value: compressed aggregator state │
│ │
│ Timeline: │
│ 12:00 ──[snapshot]──────────────────── │
│ 12:05 ──────────────[snapshot]───────── │
│ 12:10 ──────────────────────[snapshot] │
└─────────────────────────────────────────┘
Mechanism 2: Property-Level Mutation Tracking
┌─────────────────────────────────────────┐
│ Only store CHANGED properties │
│ │
│ 12:03 - health_status: "degraded" │
│ 12:07 - error_rate: 0.15 │
│ 12:09 - health_status: "down" │
│ │
│ Much more efficient than full copies │
└─────────────────────────────────────────┘
Mechanism 3: Query-Time Reconstruction
┌─────────────────────────────────────────┐
│ To query topology at 12:08: │
│ │
│ 1. Load snapshot from 12:05 │
│ 2. Apply mutations from 12:05-12:08 │
│ 3. Return reconstructed state │
│ │
│ Fast: indexed lookups, no log replay │
└─────────────────────────────────────────┘
Full Snapshots:
Every 5 min × 288 snapshots/day × full graph size
= Exponential storage growth ❌
Event Sourcing (log replay):
All events stored, replay from beginning
= Slow reconstruction ❌
Combined Approach (Netflix):
Sparse snapshots + sparse mutations
= Efficient storage + fast queries ✅
This is the most important meta-lesson:
Fix Kafka Lag
↓
Discover Hot Nodes
↓
Fix Hot Nodes
↓
Discover GC Pressure
↓
Fix GC Pressure
↓
Discover Serialization Issues
↓
Fix Serialization
↓
Discover Write Distribution Problems
↓
Fix Write Distribution
↓
Next bottleneck...
This is not failure. This is how distributed systems are built.
┌─────────────────────────────────────────────┐
│ OPTIMIZATION LOOP │
│ │
│ 1. MEASURE │
│ Profile CPU, memory, latency, GC │
│ Use: async-profiler, heap dumps, │
│ Kafka consumer metrics │
│ │
│ 2. HYPOTHESIZE │
│ Identify root cause from data │
│ Don't guess — let metrics guide you │
│ │
│ 3. FIX │
│ Target the specific bottleneck │
│ Make one change at a time │
│ │
│ 4. VALIDATE │
│ Confirm improvement with metrics │
│ Ensure no regression elsewhere │
│ │
│ 5. REPEAT │
│ Move to next bottleneck │
└─────────────────────────────────────────────┘
Best Practice: Use immutable data structures
Reality at scale: Creates GC pressure at millions of
allocations/second
Best Practice: Use gRPC for service communication
Reality at scale: Heavyweight for streaming aggregation
Best Practice: Single-stage aggregation is simpler
Reality at scale: Creates hot nodes with skewed data
Rule: Best practices are starting points.
Measurement justifies deviation.
Document why you deviated.
SERVICE TOPOLOGY AT SCALE
│
├── WHY STREAMING?
│ └── Batch = hours old → useless for incidents
│ Streaming = minutes old → actionable
│
├── HOW TO HANDLE LOAD?
│ └── Backpressure → slow down gracefully, never drop data
│
├── HOW TO RESOLVE NETWORK HOPS?
│ └── Three-stage pipeline
│ Stage 1: Compress (aggregation)
│ Stage 2: Resolve (intermediary removal)
│ Stage 3: Enrich + Persist
│
├── HOW TO DISTRIBUTE LOAD?
│ └── Dynamic consistent hashing via service registry
│ Multi-stage redistribution for skewed data
│
├── HOW TO QUERY HISTORY?
│ └── Snapshots + Mutations + Query-time reconstruction
│
└── HOW TO OPTIMIZE?
└── Measure → Hypothesize → Fix → Validate → Repeat
Break conventions when data justifies it