DEV Community

Le Beltagy
Le Beltagy

Posted on

Cloud Architecture for Autonomous Systems: The 3-Tier Design & Why It Works

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                    │                │
│  └────────────────────────────────────────┘                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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:

  1. Users expect dashboards. Giving them a custom React dashboard = I maintain it forever.
  2. Grafana solves this. Pre-built panels, alerting, user management, all free.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Redis Specific Use Cases:

  1. Session Storage: User login tokens (TTL: 24 hours)
  2. Real-Time Streams: Vehicle location updates (using Redis Streams)
  3. Query Cache: "Latest metrics for vehicle V123" (TTL: 30 seconds)
  4. 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)   │
└───────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)