Production Architecture (TingTing / Zoober): Internet │ ▼ CloudFront CDN ──── WAF (OWASP rules, rate limiting) │ │ blocks bad traffic ▼ │ ALB (Application Load Balancer) │ ├─── Target Group A (Blue) ←── active │ EC2 t3.medium │ NGINX → Node.js :3000 │ └─── Target Group B (Green) ←── standby / new release EC2 t3.medium NGINX → Node.js :3000 │ ├─── MongoDB Atlas (replica set, 3 nodes) ├─── Redis (ElastiCache or self-hosted) └─── S3 (assets) ← CloudFront OAC

EC2 — Instance Selection & Strategy

Why t3/t3a for Node.js?

Interview Q&A

Q: Why not use Lambda or ECS instead of EC2?

A: WebSocket connections require persistent, long-lived TCP connections — Lambda's 15-min max invocation time and ECS Fargate's cold start overhead make them unsuitable. EC2 gives us full control over the process lifecycle, connection pool warmup, and PM2-based cluster mode. For stateless APIs, Lambda would be fine, but the real-time WebSocket layer needs EC2.

NGINX — Reverse Proxy Configuration

# /etc/nginx/sites-available/app
upstream nodejs {
    server 127.0.0.1:3000;
    keepalive 64;  # keep upstream connections alive — avoids TCP handshake per req
}

server {
    listen 443 ssl http2;
    server_name api.zoober.in;

    # SSL termination here — Node.js handles plain HTTP internally
    ssl_certificate     /etc/letsencrypt/live/api.zoober.in/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.zoober.in/privkey.pem;

    # Gzip compression for JSON responses
    gzip on;
    gzip_types application/json text/plain;
    gzip_min_length 1000;

    # Rate limiting (token bucket) — 100 req/s per IP, burst 200
    limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;

    location /api/ {
        limit_req zone=api burst=200 nodelay;
        proxy_pass http://nodejs;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;    # WebSocket upgrade
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 86400;  # WebSocket long-lived connections
    }

    # Static files served directly by NGINX — bypasses Node.js entirely
    location /public/ {
        root /var/www/app;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}
Interview Q&A

Q: What does keepalive 64 do in the upstream block?

A: Without keepalive, NGINX opens a new TCP connection to Node.js for every proxied request (connect → request → close). With keepalive 64, NGINX maintains a pool of 64 persistent connections to the upstream. This eliminates TCP handshake overhead (~1-3ms) on every request, directly improving P95 latency.

CloudFront — CDN & Cache Strategy

What CloudFront does in this architecture

Cache behaviors by path

Invalidation strategy

When a store updates their catalog, the backend calls CloudFront.createInvalidation({ paths: ['/catalog/store-id/*'] }). Wildcard invalidations cost $0.005 each, so they're triggered only on catalog updates, not on every order.

Signed URLs for private content

// Generate a pre-signed URL for a private S3 object via CloudFront
const { getSignedUrl } = require('@aws-sdk/cloudfront-signer');

const url = getSignedUrl({
  url: `https://cdn.zoober.in/uploads/${filename}`,
  keyPairId: process.env.CF_KEY_PAIR_ID,
  privateKey: process.env.CF_PRIVATE_KEY,
  dateLessThan: new Date(Date.now() + 15 * 60 * 1000), // 15min expiry
});

WAF — Web Application Firewall

Rule groups in use

Interview Q&A

Q: Why have both WAF rate limiting AND NGINX rate limiting?

A: Defense in depth. WAF (CloudFront layer) blocks abuse before it reaches our EC2 instances — this protects against DDoS at the CDN edge. NGINX rate limiting is the second layer at the instance level, for cases where traffic bypasses CloudFront (e.g., direct IP access). Two layers means an attacker can't take down the origin even if they find the EC2 IP.

S3 — Storage Architecture

Bucket structure

Presigned URL upload flow

// Client requests upload URL from backend
// Backend generates presigned URL (never exposes S3 credentials to client)
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

const url = await getSignedUrl(s3Client, new PutObjectCommand({
  Bucket: 'zoober-private-uploads',
  Key: `riders/${riderId}/${filename}`,
  ContentType: 'image/jpeg',
}), { expiresIn: 300 }); // 5 min

// Client uploads directly to S3 using presigned URL — zero load on our server
// Backend then updates DB with S3 key after client confirms upload

SES — Transactional Email

Use cases

Deliverability setup

Auto-Scaling

ASG configuration

Blue-Green Deployments

Deployment flow: Step 1: Both environments running, Blue is active (100% traffic) ALB → Blue (100%) → Green (0%) Step 2: Deploy new version to Green ALB → Blue (100%) → Green (0%, updating) Step 3: Run smoke tests against Green directly (via ALB weighted routing) ALB → Blue (90%) → Green (10%, testing) Step 4: Smoke tests pass → shift all traffic ALB → Blue (0%) → Green (100%) Step 5: Keep Blue warm for 15min (rollback window) If health checks fail → ALB → Blue (100%) in <30s
Interview Q&A

Q: What triggers an automatic rollback?

A: ALB monitors the Green target group's health check endpoint (/health returning 200). If more than 20% of health checks fail within 2 minutes of the traffic shift, the deployment pipeline triggers a rollback by shifting the ALB listener rule back to Blue. The whole rollback takes <30 seconds — faster than any manual intervention.

Q: What's in the smoke test?

A: Basic end-to-end checks: (1) GET /health returns 200, (2) MongoDB connection is alive, (3) Redis is reachable, (4) A known store's catalog returns data, (5) Auth endpoint issues a valid JWT. These run against the Green environment before any real traffic hits it.