Cloud Architecture for Autonomous Systems: The 3-Tier Design & Why It Works
How I designed a production-grade system for handling real-time vehicle data at scale
The Problem I Solved
When I started building VehicleMetrics, I faced a choice that every backend engineer faces when tackling real-time systems:
Do I over-engineer and build something that scales to millions? Or do I start lean and regret it when things break?
I chose a third path: build a system that scales intelligently from day one, but doesn't waste resources on premature optimization.
This is the story of how I designed a 3-tier cloud architecture that handles autonomous vehicle data in real-time, scales horizontally, maintains data integrity, and stays operationally simple enough for one developer to manage.
Why 3 Tiers? Why Not Serverless? Why Not Monolith?
Let me be honest: I considered all approaches.
Serverless (Lambda/DynamoDB)
Pros: No servers to manage. Auto-scales. Pay per invocation.
Cons: Cold starts (critical for real-time data). Vendor lock-in. Query limitations with DynamoDB. Debugging nightmares when things go wrong.
Decision: No. Real-time vehicle data can't tolerate 500ms cold starts.
Monolith (Single large service)
Pros: Simple to deploy. Easier debugging. Single database.
Cons: Can't scale individual components. Technology lock-in. Deployment = all-or-nothing risk.
Decision: No. Different components scale differently. Data ingestion ≠ API response times.
3-Tier Microservices
Pros: Independent scaling. Technology flexibility. Clear separation of concerns. Battle-tested pattern.
Cons: More complex. Requires orchestration (Kubernetes). Multiple databases = consistency challenges.
Decision: Yes. This is the Goldilocks zone.
The Architecture: Layer by Layer
┌─────────────────────────────────────────────────────────────┐
│ PRESENTATION TIER │
│ React + TypeScript + Grafana Dashboards │
│ (Real-time Analytics UI) │
│ │
│ • User Dashboards (React + WebSocket) │
│ • Grafana (System Metrics & Alerts) │
│ • Real-time Analytics Display │
└─────────────────┬───────────────────────────────────────────┘
│
┌────────┴──────────┐
│ HTTPS + WebSocket │
│ TLS 1.3 │
└────────┬──────────┘
│
┌─────────────────▼───────────────────────────────────────────┐
│ APPLICATION TIER │
│ FastAPI Microservices on EKS (Kubernetes) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Ingestion Svc │ │ Analytics Svc │ │
│ │ (High throughput)│ │ (Complex queries)│ │
│ │ Auto-scale: 1-10 │ │ Auto-scale: 1-5 │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Auth Svc │ │ Aggregation Svc │ │
│ │ (Stateless) │ │ (Batch processes)│ │
│ │ Auto-scale: 1-3 │ │ Auto-scale: 1-2 │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ All behind: AWS Application Load Balancer │
│ All in: Private subnets with NAT gateway │
└─────────────────┬───────────────────────────────────────────┘
│
┌───────────┼───────────┐
│ │ │
(Private Network - VPC)
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ DATA TIER │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ PostgreSQL 14 + TimescaleDB │ │
│ │ • Time-series data (optimized) │ │
│ │ • Retention: 90 days hot, 1yr archive │ │
│ │ • Backup: Continuous PITR │ │
│ │ • Read replicas for analytics queries │ │
│ │ • Multi-AZ for high availability │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ Redis 7 (Cluster Mode) │ │
│ │ • Real-time data streams │ │
│ │ • Session storage │ │
│ │ • Cache layer for hot queries │ │
│ │ • 6 nodes, 1GB each │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ S3 Data Lake │ │
│ │ • Raw sensor data (parquet format) │ │
│ │ • Lifecycle: 30 days → Glacier │ │
│ │ • Partition by vehicle_id/date │ │
│ │ • Versioning enabled │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ Kinesis Data Streams │ │
│ │ • Real-time ingest (1000 rps) │ │
│ │ • Lambda consumers for processing │ │
│ │ • 24-hour retention │ │
│ └────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Tier 1: Presentation (Frontend)
Why React + TypeScript?
React:
- Component-driven = easier to maintain
- Virtual DOM = predictable performance
- Ecosystem = everything you need exists
- WebSocket support = real-time data
TypeScript:
- Catches bugs at compile time
- Self-documenting code
- Refactoring confidence
- Better IDE support
Why Grafana Dashboards?
Grafana wasn't in the original plan. But I realized:
- Users expect dashboards. Giving them a custom React dashboard = I maintain it forever.
- Grafana solves this. Pre-built panels, alerting, user management, all free.
- It integrates. Prometheus metrics flow directly into Grafana.
Decision: Custom React for business dashboards. Grafana for ops/metrics dashboards.
Connection Pattern: WebSocket
Why not REST polling?
REST Polling:
Client → POST /api/latest → Server (every 5 seconds)
= 12 requests per minute per client
= 12,000 requests/min with 1000 clients
= Unnecessary load
WebSocket (Server-Sent Events):
Server opens persistent connection
Server pushes updates when available
= Only real changes transmitted
= 1/10th the bandwidth
Decision: WebSocket for real-time data. REST for historical queries.
Tier 2: Application (Backend Microservices)
Why Microservices? Why Not One Giant Service?
Here's the problem with monoliths:
Ingestion Service receives 1,000 requests/second of sensor data. It needs:
- Minimal processing
- Immediate response
- Horizontal scaling
Analytics Service runs complex queries across months of data. It needs:
- Heavy CPU
- Large memory
- Fewer instances (expensive to scale)
These services have opposite needs.
A monolith forces you to scale the entire application. Microservices let you scale only what you need.
Service Breakdown
1. Ingestion Service
Endpoint: POST /api/vehicles/{id}/sensor-data
Input: Raw sensor telemetry (30KB per message)
Processing: Validation + De-duplication
Output:
- Write to Kinesis (real-time stream)
- Write to PostgreSQL (persistent storage)
Response: 201 Created (< 50ms)
Scaling: Auto-scale 1-10 instances
Reasoning: This is your bottleneck. High throughput, simple logic.
Why this design?
- Fire-and-forget writes to Kinesis = immediate response
- Async processing = no blocking operations
- Database writes are asynchronous (Lambda consumes Kinesis)
- One instance handles 100+ messages/second
2. Analytics Service
Endpoint: GET /api/analytics/vehicle/{id}?from=date&to=date
Input: Query parameters (dates, filters)
Processing: Complex SQL joins, aggregations
Output: Processed metrics (vehicle efficiency, anomalies, trends)
Response: 200 OK (may take 2-5 seconds)
Scaling: Auto-scale 1-5 instances
Reasoning: CPU-bound, fewer instances needed, heavy queries.
Why this design?
- Queries run against read replicas (no impact on write performance)
- Results cached in Redis (repeat queries = 50ms response)
- Batch processing at off-peak hours
- One instance per 5-10 concurrent users
3. Auth Service
Endpoint: POST /api/auth/token
Input: credentials or refresh_token
Processing: OAuth2/OIDC with AWS Cognito
Output: JWT token + claims
Response: 200 OK (< 100ms)
Scaling: Auto-scale 1-3 instances
Reasoning: Stateless, lightweight. Rarely the bottleneck.
Why this design?
- Stateless = trivial to scale
- Outsourced to AWS Cognito = we validate JWTs
- No database lookups
- Can run anywhere
4. Aggregation Service
Schedule: Every 10 minutes
Input: Raw sensor data from yesterday
Processing: Compress + rollup data
Output: Daily summaries written to cold storage
Storage: S3 (cheap long-term)
Scaling: Batch job, auto-scale 1-2 instances
Reasoning: Runs off-peak, one instance sufficient.
Why this design?
- PostgreSQL only keeps 90 days hot
- S3 keeps 1 year (Parquet format = 1/10th the space)
- Old data queries redirect to S3 (Athena)
- Cost reduction: hot vs cold data storage
Orchestration: Kubernetes (EKS)
Why not Docker Compose?
Docker Compose:
- ✅ Great for local development
- ✅ Simple deployments
- ❌ No auto-scaling
- ❌ No self-healing
- ❌ No rolling updates
- ❌ Doesn't survive node failures
Kubernetes (EKS):
- ✅ Auto-scaling based on CPU/memory
- ✅ Self-healing (pod dies? restart automatically)
- ✅ Rolling updates (zero downtime)
- ✅ Multi-AZ resilience
- ✅ Proven at scale
Decision: EKS for production. Docker Compose for local dev + CI/CD testing.
Load Balancing: ALB (Application Load Balancer)
Why ALB over NLB?
NLB (Network Load Balancer):
- Ultra-high throughput (millions/sec)
- Layer 4 (connection-level)
- Cost: High
ALB (Application Load Balancer):
- Layer 7 (application-level)
- URL-based routing
- Cost: Reasonable
Your load: ~5,000 requests/second total
→ ALB handles this easily
→ Save money
If you hit 50,000+ requests/second:
→ Upgrade to NLB
Decision: ALB. Easy path to upgrade if needed.
Tier 3: Data (Persistence & Processing)
PostgreSQL + TimescaleDB: Why Time-Series Specialization?
Standard PostgreSQL for time-series data is... bad.
Your data:
- Millions of rows per day
- Queries: "show me data from this vehicle last 30 days"
- Traditional indexing: SLOW
TimescaleDB advantage:
- Hypertables = automatic partitioning by time
- Native time-series functions (rollup, downsample)
- 10-100x faster queries
- Compression = 1/10th storage
Example:
-- Standard PostgreSQL
SELECT AVG(speed) FROM sensor_data
WHERE vehicle_id = 'V123'
AND timestamp > NOW() - INTERVAL '30 days'
-- Scans millions of rows, slow
-- TimescaleDB
SELECT time_bucket('1 hour', timestamp) as hour,
AVG(speed)
FROM sensor_data
WHERE vehicle_id = 'V123'
AND timestamp > NOW() - INTERVAL '30 days'
GROUP BY hour
-- Uses hypertable partitions, 100x faster
Data Retention Strategy:
Hot (PostgreSQL): 90 days
- Real-time queries
- Full resolution
- Fast
Warm (S3 Parquet): 1 year
- Historical analysis
- Aggregated
- Cheap (Glacier)
Cold (Archival): Indefinite
- Regulatory compliance
- Deep archive (AWS Glacier Deep Archive)
Redis: Why Not Just Use PostgreSQL Cache?
PostgreSQL has built-in caching...
Real-time dashboard needs latest speed for vehicle V123:
- Query PostgreSQL: 200ms (disk I/O)
- Query Redis: 2ms (memory)
- 100x difference
With 1000 concurrent users:
- PostgreSQL: Database collapses
- Redis: Handles trivially
Redis Specific Use Cases:
- Session Storage: User login tokens (TTL: 24 hours)
- Real-Time Streams: Vehicle location updates (using Redis Streams)
- Query Cache: "Latest metrics for vehicle V123" (TTL: 30 seconds)
- Rate Limiting: "API key used 950/1000 requests" (TTL: 1 hour)
Why Cluster Mode?
Single Redis instance:
- Holds entire dataset in memory
- 6GB vehicle data = 6GB RAM
- Cost: ~$0.50/hour
Redis Cluster (3 nodes):
- Partitions data across nodes
- Each node holds 2GB
- More resilient
- Cost: ~$0.80/hour (33% premium for resilience)
Decision: Cluster mode. The 33% cost for high availability is worth it.
Kinesis: Real-Time Data Pipeline
Why not just write directly to PostgreSQL?
Direct writes:
POST /ingest → PostgreSQL (synchronous)
Problems:
- Ingest service waits for disk I/O
- Database is bottleneck
- Can't exceed database write throughput
Kinesis pattern:
POST /ingest → Kinesis (fast, async)
Lambda → PostgreSQL (asynchronous processing)
Benefits:
- Ingest responds in 50ms (only writes to Kinesis buffer)
- Database is decoupled
- Can retry failed writes
- Can replay data if needed
- Can add new consumers without changing ingest
Why 1,000 requests/second is our target:
Single vehicle:
- GPS: 10Hz (10 updates/second)
- Sensors: 100Hz (speed, temp, pressure, etc)
- 1 vehicle = 110 messages/second
50 vehicles in production:
50 × 110 = 5,500 messages/second needed
Kinesis shard handles 1,000/sec
→ Need 6 shards
→ Cost: ~$300/month
1 million messages/second:
→ 1,000 shards
→ Cost: $300,000/month (enterprise scale)
Decision: Kinesis with on-demand scaling. Pay per GB ingested.
Design Decisions I Made (and Why)
1. Private Subnets for Everything
Why? Reduces attack surface. No direct internet access to databases.
Internet → ALB (public subnet)
→ EKS (private subnet)
→ RDS (private subnet)
Attacker gets ALB? Can't reach databases directly.
2. Multi-AZ Deployment
AWS Region (e.g., us-east-1)
├── AZ 1: RDS Primary, EKS nodes, Redis nodes
├── AZ 2: RDS Replica, EKS nodes, Redis nodes
└── AZ 3: EKS nodes, backup
One AZ goes down?
→ System still running (automatic failover)
3. Stateless Services
Every service is 100% stateless:
- No local files
- No in-memory caches
- No session storage
Why? Easy horizontal scaling. Kill pod, create new one, no state loss.
4. API Rate Limiting
Per API key: 1,000 requests/minute
Enforced at: ALB + Auth service
Benefit: Prevents one bad actor from crushing system
Data Flow: A Single Vehicle's Update
┌─────────────────────────────────────────────────────────────┐
│ 1. Vehicle Telemetry (GPS + 50 sensors) │
│ Sent: Every 100ms │
│ Payload: ~30KB │
└─────────────────┬───────────────────────────────────────────┘
│ HTTPS
┌─────────────────▼───────────────────────────────────────────┐
│ 2. ALB (Application Load Balancer) │
│ • Rate limit check │
│ • Route to Ingestion service │
└─────────────────┬───────────────────────────────────────────┘
│
┌─────────────────▼───────────────────────────────────────────┐
│ 3. Ingestion Service (Pod) │
│ • Validate payload (< 5ms) │
│ • De-duplicate (Redis check) │
│ • Write to Kinesis (async, < 10ms) │
│ • Return 201 Created (< 50ms total) │
└─────────────────┬───────────────────────────────────────────┘
│
┌──────────────┴───────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Kinesis Stream │ │ Redis Cache │
│ (Real-time) │ │ (Latest value) │
└─────────┬────────┘ └──────────────────┘
│
┌─────────▼─────────────────────────────────────────────────┐
│ 4. Lambda (Kinesis Consumer) │
│ • Runs every 1 second │
│ • Batch processes messages (efficiency) │
│ • Writes to PostgreSQL + TimescaleDB │
│ • Updates Redis streams (real-time feed) │
└─────────┬───────────────────────────────────────────────┘
│
┌─────────▼───────────────────────────────────────────────────┐
│ 5. Dashboard (User's Browser) │
│ • Subscribes to Redis Streams (WebSocket) │
│ • Receives updates instantly │
│ • Displays latest metrics │
│ • Historical queries hit PostgreSQL (cached in Redis) │
└───────────────────────────────────────────────────────────────┘
Total latency: ~100ms from vehicle sensor to dashboard display
Cost Breakdown (Rough Monthly)
Service Usage Cost
─────────────────────────────────────────────
EKS Compute 6 nodes (t3.medium) $150
RDS PostgreSQL db.t3.small + replica $100
ElastiCache Redis 3-node cluster $60
S3 Storage 100GB per month $2
Kinesis Streams 5-10M records $25
Lambda Processing 1B invocations $20
Data Transfer ~50GB out $5
NAT Gateway 200GB $30
─────────────────────────────────────────────
TOTAL (monthly): ~$392
Annual: ~$4,700 for production system
What I'd Change If Building Today
1. Add a Message Queue (SQS)
Between Ingestion and Kinesis:
Current: POST → Kinesis → Lambda
Problem: Kinesis can get backlogged
Better: POST → SQS → Kinesis → Lambda
Benefit: SQS is cheaper, provides buffering
2. Use EventBridge Instead of Kinesis
EventBridge (AWS's event bus) is newer and:
- More flexible routing
- Better filtering at source
- Native integration with Lambda, SNS, etc.
3. Kafka for Very High Volume
If hitting 100,000+ messages/second:
Kinesis: $300,000+/month
Kafka (self-managed): $2,000/month
Kafka (Confluent managed): $10,000/month
Lessons for Your Architecture
1. Match Tool to Problem
- Low latency, high throughput: Use Kinesis + Lambda
- Complex queries, low throughput: Use PostgreSQL + read replicas
- Session state, caching: Use Redis
- Long-term storage: Use S3 with Parquet
Don't try to use one tool for everything.
2. Separate Read and Write Paths
Writes are different from reads:
- Writes: Fast, simple, durable (Kinesis)
- Reads: Complex, slow, can be stale (Redis cache)
Build them separately.
3. Auto-Scaling Is Non-Negotiable
For autonomous vehicle data, you can't predict demand:
3 AM Tuesday: 0 vehicles
3 PM Friday: 50 vehicles
3 AM Sunday: 10 vehicles
Every service must auto-scale based on load.
4. Think in Tiers
- Tier 1 (Hot): Everything in memory, milliseconds
- Tier 2 (Warm): Disk-based, seconds
- Tier 3 (Cold): Archive, days
This matches cost to need.
Next Steps
This architecture handles Phase 1 comfortably. For Phase 2/3:
- Add ML pipeline: Model training on historical data
- Add notifications: Alert when anomalies detected
- Add analytics: Trend analysis across vehicles
- Multi-region: Deploy to multiple AWS regions
TL;DR
- 3-tier architecture separates concerns: frontend, backend, data
- Microservices scale independently (1,000 rps ingestion ≠ analytics queries)
- Kubernetes provides auto-scaling, self-healing, and resilience
- Polyglot persistence: PostgreSQL (structured) + Redis (real-time) + S3 (archive)
- Async pipelines: Kinesis decouples ingest from processing
- Cost: ~$400/month for production system handling 50 vehicles
GitHub: beltagyy/vehicle-metrics
Author: Mohamed ElBeltagy (@beltagyy)
Topic: Cloud Architecture | Microservices | Autonomous Systems
Top comments (0)