Skip to main content

Chapter 10: Cloud Engineering for Staff Engineers

You don't need an AWS certification. You need to know exactly twelve services — and how they compose into production systems that serve millions of users without waking you up at 3 AM.

I have watched engineers spend six months grinding through A Cloud Guru courses, memorizing the difference between EBS volume types, collecting a Solutions Architect Associate badge, and still freezing when asked to design a multi-region deployment for a payment processing system. The certification teaches you what every service does. It does not teach you how they fit together. It does not teach you what breaks at scale. It does not teach you the judgment to know when not to use a service.

That judgment is what separates a Staff Engineer from a senior developer who deploys to someone else's infrastructure.

Here is the uncomfortable truth about the Indian tech market in 2026: companies paying ₹60 lakh to ₹1 crore are not hiring you to write more code. They are hiring you to own systems end-to-end. That means the database, the message queue, the network topology, the deployment pipeline, the cost optimization, the incident response. If your mental model of "the cloud" stops at git push heroku main or a CI/CD pipeline someone else configured, you are capped at ₹40-45 LPA. The ceiling is real, and it is made of infrastructure you cannot reason about.

This chapter will not make you an AWS expert. It will make you dangerous enough to design production systems, ask the right questions in architecture reviews, and pass the infrastructure rounds at top-tier interviews. We will cover twelve services. For each one, you will learn what it does, when to use it, what to avoid, and how to wire it up from Node.js. By the end, you will have a mental model of how cloud services compose — and that model is worth more than any certification.

The Twelve Services That Matter

Before we dive in, here is the list. Memorize it. These are the only AWS services that come up in every Staff Engineer interview and every production incident at scale.

  1. EC2 — virtual machines, the foundation
  2. ECS / EKS — container orchestration
  3. Lambda — serverless compute
  4. VPC — your private network in the cloud
  5. ALB / NLB — load balancers
  6. S3 — object storage
  7. RDS — relational databases, managed
  8. DynamoDB — NoSQL at any scale
  9. ElastiCache — Redis/Memcached, managed
  10. SQS / SNS / EventBridge — messaging and events
  11. API Gateway — HTTP front door for serverless
  12. IAM — permissions for everything

That is it. Not Redshift. Not Kinesis. Not Step Functions. Not AppSync. Those are useful services. They are not the twelve you need to know cold. Master these twelve, and you can reason about any system built on AWS. The rest you can learn in an afternoon when you need them.

Let us start at the bottom and work up.


Compute: Where Your Code Actually Runs

EC2: The Building Block

EC2 is a virtual machine in a data center. You pick an instance type, an AMI (machine image), a security group, and a subnet. AWS bills you per second. That is the entire abstraction.

When should you use EC2 directly? Almost never in 2026. The industry has moved to containers and serverless. But you need to understand EC2 because ECS and EKS run on it, because Lambda cold starts are solved by keeping EC2 instances warm, and because some workloads — GPU training, legacy monoliths, latency-sensitive stateful services — still run best on raw VMs.

Here is what you actually need to know about EC2 for a Staff Engineer role:

Instance families. Not all vCPUs are equal. C-family (compute-optimized) for CPU-bound work. R-family (memory-optimized) for in-memory caches and databases. M-family (general purpose) for web servers. Know these three families. The rest you can look up.

Placement groups. If you need sub-millisecond latency between instances, put them in a cluster placement group within the same Availability Zone. This is relevant for high-frequency trading, real-time bidding, and multiplayer game servers. For most applications, ignore placement groups.

Spot instances. AWS sells unused capacity at up to 90% discount, with a two-minute termination notice. Use spot for fault-tolerant workloads: batch processing, CI/CD runners, dev environments. Never run your primary database on spot. I have seen a Bangalore fintech startup learn this the hard way when their entire RDS cluster evaporated mid-payment-cycle because someone thought "90% savings" was worth the risk.

Auto Scaling Groups (ASGs). Define a minimum, maximum, and desired count of instances. ASG replaces failed instances and scales based on CloudWatch metrics. The key insight: ASGs are for horizontal scaling. If your application cannot scale horizontally, no amount of ASG configuration will save you.

Here is a Node.js snippet that launches an EC2 instance using the AWS SDK v3. This is not production code — it is a learning tool to understand the API surface.

import { EC2Client, RunInstancesCommand } from "@aws-sdk/client-ec2";

const ec2 = new EC2Client({ region: "ap-south-1" });

async function launchWebServer() {
const command = new RunInstancesCommand({
ImageId: "ami-0a1b2c3d4e5f6g7h8", // Amazon Linux 2023 in ap-south-1
InstanceType: "t3.medium",
MinCount: 1,
MaxCount: 1,
KeyName: "my-key-pair",
SecurityGroupIds: ["sg-0abc123def456"],
SubnetId: "subnet-0abc123def456",
TagSpecifications: [
{
ResourceType: "instance",
Tags: [
{ Key: "Name", Value: "web-server-01" },
{ Key: "Environment", Value: "production" },
],
},
],
});

const response = await ec2.send(command);
const instanceId = response.Instances[0].InstanceId;
console.log(`Launched ${instanceId}`);
return instanceId;
}

Notice the tags. Tag everything. In a company with fifty microservices across three environments, untagged resources are technical debt that compounds daily. A Staff Engineer enforces tagging from day one.

ECS vs EKS: The Container War, Settled

You have two choices for running containers on AWS: ECS (Elastic Container Service) and EKS (Elastic Kubernetes Service). The decision is simpler than most engineers think.

Use ECS when: You are a small-to-medium team (under 50 engineers), you do not need the Kubernetes ecosystem (Helm charts, operators, service meshes), and you want AWS to handle the control plane. ECS is deeply integrated with AWS — it understands IAM roles natively, it wires into ALB with minimal configuration, and it has no control plane cost. You pay only for the underlying EC2 or Fargate resources.

Use EKS when: You are a larger organization with existing Kubernetes expertise, you need multi-cloud portability, or you depend on Kubernetes-native tooling (ArgoCD, Istio, cert-manager). EKS charges $0.10/hour per cluster for the control plane, which is negligible at scale but annoying for side projects.

Use neither when: Your workload is event-driven, spiky, or low-traffic. That is what Lambda is for.

The real decision is not ECS vs EKS. It is EC2 launch type vs Fargate.

With EC2 launch type, you manage the underlying instances. You are responsible for patching, scaling, and optimizing instance types. This gives you control and lower cost at high utilization. With Fargate, AWS manages the instances. You specify CPU and memory per task, and AWS places them. Fargate costs more per vCPU-hour but eliminates instance management entirely.

Here is the rule of thumb I give every team I advise: start with Fargate. The operational overhead of managing EC2 instances is not worth it until your monthly compute bill crosses ₹5 lakh. At that point, the savings from reserved instances and better bin-packing justify the complexity. Before that threshold, you are optimizing a cost that is smaller than the salary of the engineer doing the optimization.

Here is an ECS task definition for a Node.js service, using Fargate:

{
"family": "api-service",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123456789:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::123456789:role/api-service-task-role",
"containerDefinitions": [
{
"name": "api",
"image": "123456789.dkr.ecr.ap-south-1.amazonaws.com/api:latest",
"portMappings": [{ "containerPort": 3000, "protocol": "tcp" }],
"environment": [
{ "name": "NODE_ENV", "value": "production" },
{ "name": "AWS_REGION", "value": "ap-south-1" }
],
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:ap-south-1:123456789:secret:db-cred"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/api-service",
"awslogs-region": "ap-south-1",
"awslogs-stream-prefix": "api"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3
}
}
]
}

Three things to notice in this definition. First, the separation of executionRoleArn (what ECS needs to pull images and write logs) from taskRoleArn (what your application needs to access S3, DynamoDB, etc.). This is least privilege in practice. Second, secrets come from Secrets Manager, not environment variables. Never put database passwords in plaintext. Third, the health check. Without it, ECS does not know if your container is actually serving traffic or just running a process that is stuck in an infinite loop.

Lambda: The Right Tool for the Right Problem

Lambda is the most misunderstood service in AWS. Engineers either use it for everything ("serverless all the things!") or avoid it entirely ("cold starts will kill my latency"). Both positions are wrong.

Lambda is a compute service that runs your function in response to an event. You upload code. AWS runs it. You pay per millisecond of execution time. No servers to manage. Automatic scaling from zero to thousands of concurrent executions.

When Lambda shines: Event-driven workloads. API backends behind API Gateway. File processing triggered by S3 uploads. Scheduled jobs (cron replacement). Glue code between AWS services. Any workload with unpredictable or spiky traffic patterns.

When Lambda is the wrong choice: Long-running tasks (over 15 minutes — the hard timeout). Workloads requiring GPU access. Applications with consistent, predictable high traffic (EC2/Fargate is cheaper at steady state). Anything requiring persistent connections (WebSockets are possible via API Gateway but painful).

Cold starts are real, but manageable. A cold start happens when Lambda needs to initialize a new execution environment. For Node.js, this is typically 200-500ms. For Java, it can be 2-3 seconds. The fix is not "avoid Lambda." The fix is:

  1. Provisioned Concurrency — keep N instances warm at all times. Costs more, eliminates cold starts for known traffic levels.
  2. Smaller bundles — tree-shake your dependencies. A 50MB Lambda package starts slower than a 5MB one.
  3. SnapStart (Java) — AWS pre-initializes the runtime and snapshots it. Not relevant for Node.js, but know it exists.
  4. Architecture choice — if your API has 10,000 requests per second at steady state, Lambda is the wrong choice regardless of cold starts. Use ECS/EKS.

Here is a Lambda function that processes S3 uploads — a classic pattern:

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";

const s3 = new S3Client({ region: process.env.AWS_REGION });
const dynamo = new DynamoDBClient({ region: process.env.AWS_REGION });

export async function handler(event) {
const record = event.Records[0];
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));

console.log(`Processing ${key} from ${bucket}`);

// Fetch the uploaded file
const { Body } = await s3.send(
new GetObjectCommand({ Bucket: bucket, Key: key })
);
const content = await Body.transformToString("utf-8");

// Parse and store metadata
const metadata = JSON.parse(content);

await dynamo.send(
new PutItemCommand({
TableName: process.env.METADATA_TABLE,
Item: {
fileId: { S: key.split("/").pop() },
uploadedAt: { S: record.eventTime },
size: { N: String(record.s3.object.size) },
data: { S: JSON.stringify(metadata) },
},
})
);

return { statusCode: 200, body: JSON.stringify({ processed: key }) };
}

Notice what this function does not do: it does not create an HTTP server, it does not parse routes, it does not manage connections. Lambda strips away everything except the business logic. That is the point.

Lambda concurrency and cost. Lambda has a default concurrency limit of 1,000 per region. If your function receives 1,001 simultaneous requests, the 1,001st gets throttled. You can request a limit increase, but the real question is: does your downstream system (RDS, DynamoDB, downstream API) handle 1,000 concurrent connections? Lambda will happily scale up and melt your database. Always set a reserved concurrency limit that matches what your downstream can handle.

Cost-wise, Lambda is cheap for low-to-moderate traffic and expensive at high, steady traffic. The break-even point with EC2 depends on your workload, but a rough heuristic: if your function runs 100ms and receives more than 10 million invocations per month, compare pricing with Fargate. Below that, Lambda is almost certainly cheaper.


Networking: The Part Everyone Skips Until It Breaks

VPC: Your Private Island

A VPC (Virtual Private Cloud) is a logically isolated section of AWS where you launch your resources. Think of it as your own private data center, defined in software.

Every AWS account comes with a default VPC. Delete it. Create your own with a well-planned CIDR range. The default VPC is fine for tutorials. It is not fine for production systems that will grow over three years.

CIDR planning. Your VPC CIDR block determines how many IP addresses you have. A /16 gives you 65,536 addresses. That sounds like a lot until you have 50 microservices, each with 3 AZs, each with an auto-scaling group that can grow to 20 instances, plus RDS instances, plus Lambda ENIs, plus VPC endpoints. Plan for growth. Use 10.0.0.0/16 for production, 10.1.0.0/16 for staging, 10.2.0.0/16 for development. Do not use 172.16.0.0/12 unless you have a specific reason — it overlaps with Docker's default bridge network and will cause routing nightmares.

Subnets. A subnet is a range of IP addresses within your VPC, tied to a specific Availability Zone. The standard pattern:

  • Public subnets — have a route to an Internet Gateway. Resources here can receive inbound traffic from the internet. Put your load balancers and NAT gateways here. Never put your database here.
  • Private subnets — no direct internet access. Outbound internet goes through a NAT Gateway in a public subnet. Put your application servers, Lambda functions (with VPC config), and databases here.
  • Isolated subnets — no internet access at all. For compliance-heavy workloads that must be fully air-gapped.

You need at least two public and two private subnets, each in a different AZ, for high availability. Three AZs is better. Here is what that looks like in practice:

VPC: 10.0.0.0/16
├── ap-south-1a
│ ├── public-1a: 10.0.1.0/24
│ └── private-1a: 10.0.10.0/24
├── ap-south-1b
│ ├── public-1b: 10.0.2.0/24
│ └── private-1b: 10.0.20.0/24
└── ap-south-1c
├── public-1c: 10.0.3.0/24
└── private-1c: 10.0.30.0/24

NAT Gateways. A NAT (Network Address Translation) Gateway lets resources in private subnets reach the internet (to download packages, call external APIs, send emails) without being reachable from the internet. AWS charges ~₹3/hour per NAT Gateway plus data processing fees. That is roughly ₹2,200/month per NAT Gateway before any traffic. If you deploy one NAT Gateway per AZ for high availability, you are spending ₹6,600/month just on NAT Gateways.

For a startup or side project, this is painful. The cost-optimization move: use a single NAT Gateway in one AZ and accept that if that AZ goes down, your private resources lose internet access. For production systems handling revenue, deploy one per AZ and eat the cost. ₹6,600/month is cheaper than an outage during a flash sale.

Security Groups vs NACLs. Security groups are stateful firewalls at the instance level. You define inbound and outbound rules. If you allow inbound traffic on port 3000, the response traffic is automatically allowed out. NACLs (Network ACLs) are stateless firewalls at the subnet level. You must explicitly allow both inbound and outbound.

For 95% of use cases, security groups are sufficient. Use them to enforce rules like:

  • Only the ALB security group can reach the application security group on port 3000.
  • Only the application security group can reach the RDS security group on port 5432.
  • SSH (port 22) is only allowed from the bastion host security group.

NACLs are for defense-in-depth: blocking known bad IPs, isolating a compromised subnet, or meeting compliance requirements that demand subnet-level controls.

Load Balancers: ALB vs NLB

AWS offers three load balancer types. Ignore Classic Load Balancer — it is deprecated in spirit if not in letter. The real choice is ALB vs NLB.

ALB (Application Load Balancer) operates at Layer 7 (HTTP/HTTPS). It understands URLs, headers, and methods. Use it for:

  • Path-based routing (/api/* goes to one target group, /admin/* to another)
  • Host-based routing (api.example.com vs admin.example.com)
  • HTTP header-based routing
  • gRPC support
  • OIDC/OAuth authentication at the load balancer level
  • WebSocket connections

NLB (Network Load Balancer) operates at Layer 4 (TCP/UDP/TLS). It does not inspect HTTP. Use it for:

  • Ultra-low latency (NLB adds microseconds, ALB adds milliseconds)
  • Static IP addresses (NLB has a fixed IP per AZ; ALB's IP changes)
  • Non-HTTP protocols (MySQL, Redis, Kafka, custom TCP)
  • TLS termination at extreme scale (millions of connections per second)

For a typical Node.js API, use an ALB. The path-based routing alone justifies it — you can run multiple microservices behind a single ALB, routing by URL path.

Here is how you register a Node.js service with an ALB target group using the SDK:

import { ElasticLoadBalancingV2Client, RegisterTargetsCommand } from "@aws-sdk/client-elastic-load-balancing-v2";

const elbv2 = new ElasticLoadBalancingV2Client({ region: "ap-south-1" });

async function registerInstance(targetGroupArn, instanceId) {
await elbv2.send(
new RegisterTargetsCommand({
TargetGroupArn: targetGroupArn,
Targets: [{ Id: instanceId, Port: 3000 }],
})
);
console.log(`Registered ${instanceId} with target group`);
}

The critical detail: ALB health checks. Configure them carefully. A health check that is too aggressive (every 5 seconds, 2 failures = unhealthy) will flap your targets during GC pauses or deployment restarts. A health check that is too lenient (every 30 seconds, 10 failures = unhealthy) will route traffic to dead instances for five minutes. The sweet spot for most Node.js services: check every 10 seconds, mark unhealthy after 3 consecutive failures, with a 5-second timeout.


Storage: Where Your Data Lives

S3: The Universal Storage Layer

S3 (Simple Storage Service) is object storage. You put files in buckets. You get them back by key. It is the most reliable, most scalable, and most versatile service in AWS. If you learn only one AWS service deeply, make it S3.

What S3 is good for: Static assets (images, CSS, JS), log archives, data lake storage, backup and disaster recovery, hosting static websites, serving as the origin for CloudFront CDN, storing Lambda deployment packages, storing CloudTrail audit logs, storing database snapshots.

What S3 is not good for: File systems (use EFS), block storage for databases (use EBS), low-latency key-value lookups (use DynamoDB), append-only logs (use Kinesis or a dedicated log service).

S3 consistency model. As of 2020, S3 is strongly consistent for all operations. If you write an object, any subsequent read will return the latest version. This was not always true — older engineers remember "eventual consistency" horror stories. You do not need to worry about this. But you should know it changed, because interviewers still ask.

S3 storage classes. Not all data is accessed equally. S3 offers tiered pricing based on access patterns:

ClassUse CaseRetrievalCost (ap-south-1)
S3 StandardFrequently accessedInstant~₹1.8/GB/month
S3 Intelligent-TieringUnknown access patternsInstant~₹1.8/GB/month + monitoring fee
S3 Standard-IAInfrequent access (monthly)Instant, per-GB fee~₹1.0/GB/month
S3 One Zone-IARecreatable infrequent dataInstant, per-GB fee~₹0.8/GB/month
S3 Glacier Instant RetrievalQuarterly accessMilliseconds~₹0.3/GB/month
S3 Glacier Flexible RetrievalAnnual accessMinutes to hours~₹0.3/GB/month
S3 Glacier Deep ArchiveCompliance/audit (7+ years)Hours~₹0.1/GB/month

The mistake I see Indian startups make repeatedly: they store everything in S3 Standard. Logs from three years ago. User uploads that were accessed once. Database backups from deprecated services. A company I advised in Pune was spending ₹1.2 lakh/month on S3. After implementing lifecycle policies — move to Standard-IA after 30 days, Glacier after 90 days, delete after 365 days — the bill dropped to ₹28,000/month. That is ₹11 lakh saved per year. For a 30-person startup, that is real money.

S3 lifecycle policies. Automate storage class transitions. Here is a lifecycle rule that every production bucket should have:

{
"Rules": [
{
"Id": "MoveToIA",
"Status": "Enabled",
"Filter": { "Prefix": "" },
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
},
{
"Days": 90,
"StorageClass": "GLACIER"
}
],
"Expiration": {
"Days": 365,
"ExpiredObjectDeleteMarker": true
}
}
]
}

S3 versioning. Enable it. When someone accidentally deletes a production database backup, or a buggy deployment script overwrites your static assets with empty files, versioning is the difference between a 30-second recovery and a resume-updating afternoon. Versioning keeps every version of every object. Deleting an object creates a delete marker instead of actually removing the data. You can restore any previous version with a single API call.

S3 event notifications. S3 can trigger Lambda functions, SQS queues, or SNS topics when objects are created, deleted, or restored. This is the foundation of event-driven architectures on AWS. Upload a video → Lambda transcodes it. Upload a CSV → Lambda parses it and loads it into DynamoDB. Upload a log file → Lambda scans it for anomalies.

Here is a Node.js function that generates a pre-signed URL — a temporary URL that grants time-limited access to a private S3 object:

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({ region: "ap-south-1" });

async function generateDownloadUrl(bucket, key, expiresInSeconds = 3600) {
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
const url = await getSignedUrl(s3, command, { expiresIn: expiresInSeconds });
return url;
}

// Usage: a user requests their invoice PDF
// Instead of making the bucket public, generate a temporary signed URL
const invoiceUrl = await generateDownloadUrl(
"myapp-invoices",
"invoices/user-123/march-2026.pdf",
600 // valid for 10 minutes
);

Pre-signed URLs are the correct pattern for serving private content. Never make an S3 bucket public. Never hardcode AWS credentials in a frontend app. Generate a signed URL on the backend and hand it to the client.

EBS and EFS: When You Need a Filesystem

EBS (Elastic Block Store) is a virtual hard drive attached to a single EC2 instance in a single AZ. Think of it as the C: drive of your cloud VM. Use it for: database storage (RDS uses EBS under the hood), boot volumes for EC2 instances, any workload that needs a filesystem with low-latency random I/O.

EBS volumes are AZ-locked. You cannot attach a volume in ap-south-1a to an instance in ap-south-1b. This matters for disaster recovery: if an AZ fails, your EBS volumes in that AZ are unavailable until the AZ recovers. For databases, use multi-AZ RDS instead of managing EBS yourself.

EFS (Elastic File System) is NFS (Network File System) as a service. Multiple EC2 instances or Lambda functions can mount the same EFS filesystem simultaneously across multiple AZs. Use it for: shared file storage that multiple services need to read/write, content management systems, machine learning training data that multiple GPU instances share.

EFS is expensive compared to S3 — roughly 6x the cost per GB. Do not use EFS for log archives, backups, or anything that could live in S3. Use EFS only when you need a shared POSIX-compliant filesystem.


Databases: Where Your Data Survives (or Dies)

RDS: Managed Relational Databases

RDS (Relational Database Service) manages PostgreSQL, MySQL, MariaDB, Oracle, and SQL Server for you. AWS handles backups, patching, failover, and replication. You handle schema design, query optimization, and connection management.

Multi-AZ vs Read Replicas. These are different things. Multi-AZ is for high availability: a synchronous standby in a different AZ. If the primary fails, AWS fails over to the standby in 60-120 seconds. The standby does not serve traffic. Read Replicas are for read scaling: asynchronous copies that serve read queries. A read replica can be promoted to primary, but there will be data loss (the replication lag).

For production: always use Multi-AZ. The cost doubles your database bill, but the alternative is a multi-hour outage when the AZ hosting your single database instance has a problem. I have seen this happen. A Mumbai-based edtech startup ran their entire platform on a single-AZ RDS instance to save ₹8,000/month. When ap-south-1a had a partial power failure, they were down for six hours. They lost ₹22 lakh in revenue during a peak admission period. The math does not work.

Connection management. RDS has a maximum number of connections, determined by instance size and database engine. A db.t3.micro PostgreSQL instance allows ~85 connections. If you have 20 Lambda functions that each open 5 connections, you have exhausted the limit. The fix: RDS Proxy.

RDS Proxy sits between your application and RDS, pooling and reusing connections. It is essential for Lambda-based applications because Lambda functions cannot pool connections across invocations — each cold start opens new connections. RDS Proxy also handles failover gracefully, reducing failover time from minutes to seconds.

// Without RDS Proxy: each Lambda invocation opens a new connection
// This will exhaust your RDS connection pool at scale

// With RDS Proxy: connect to the proxy endpoint instead
import pg from "pg";

const pool = new pg.Pool({
host: process.env.RDS_PROXY_ENDPOINT, // proxy endpoint, not RDS endpoint
port: 5432,
database: "myapp",
user: "app_user",
password: process.env.DB_PASSWORD,
max: 5, // pool size per Lambda instance
idleTimeoutMillis: 30000,
});

export async function handler(event) {
const client = await pool.connect();
try {
const result = await client.query(
"SELECT id, name, email FROM users WHERE id = $1",
[event.pathParameters.userId]
);
return {
statusCode: 200,
body: JSON.stringify(result.rows[0]),
};
} finally {
client.release(); // always release back to the pool
}
}

Backup and point-in-time recovery. RDS automatically backs up your database daily and retains transaction logs for point-in-time recovery. The default retention period is 7 days. Increase it to 35 days for production. The cost difference is negligible. The difference between restoring to 7 days ago vs 35 days ago when someone accidentally drops a table is career-defining.

DynamoDB: NoSQL Without the Operational Pain

DynamoDB is AWS's managed NoSQL database. It is a key-value and document store with single-digit millisecond latency at any scale. There is no "instance size" to choose. You define tables, and DynamoDB scales horizontally by splitting data across partitions automatically.

When to use DynamoDB: High-scale, predictable-access-pattern workloads. User sessions. Shopping cart data. Leaderboards. Real-time clickstream. Metadata stores. Any workload where you know your access patterns upfront and need consistent low-latency at any scale.

When NOT to use DynamoDB: Ad-hoc querying (use RDS or Elasticsearch). Complex joins and aggregations (use RDS). Workloads where access patterns change frequently (DynamoDB schema changes are expensive). Full-text search (use Elasticsearch or RDS with pg_trgm).

Single-table design. This is the most important DynamoDB concept, and the one most engineers get wrong. In a relational database, you create one table per entity: users, orders, products. In DynamoDB, you put multiple entity types in a single table, using the partition key and sort key to organize and query them.

Why? Because DynamoDB charges you per table for provisioned capacity (or per RCU/WCU in on-demand mode), and because DynamoDB has no joins. If you need user data and their orders in a single request, they must be in the same table (or you make two requests, which doubles latency and cost).

Here is a single-table design for an e-commerce system:

PK (Partition Key)SK (Sort Key)TypeAttributes
USER#123PROFILE#123Username, email, phone
USER#123ORDER#2024-001Ordertotal, status, items
USER#123ORDER#2024-002Ordertotal, status, items
PRODUCT#456DETAILS#456Productname, price, stock
ORDER#2024-001ITEM#456OrderItemquantity, price

With this design, a single query with PK = USER#123 returns the user profile and all their orders. A query with PK = USER#123 AND SK begins_with("ORDER#") returns only orders. This is the pattern that makes DynamoDB fast and cost-effective.

GSIs (Global Secondary Indexes). A GSI lets you query the same data by a different key. In the table above, you can query by user ID. But what if you need to find all orders by status? Create a GSI where the partition key is status and the sort key is createdAt. Now you can query status = "PENDING" and get all pending orders.

GSIs are not free. Each GSI consumes additional write capacity and storage. But they are the only way to support multiple access patterns in DynamoDB. A well-designed DynamoDB table has 2-4 GSIs. If you need more than 5, reconsider your data model — or use RDS.

Here is a Node.js DynamoDB query using single-table design:

import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb";

const dynamo = new DynamoDBClient({ region: "ap-south-1" });

async function getUserWithOrders(userId) {
const { Items } = await dynamo.send(
new QueryCommand({
TableName: "ECommerceApp",
KeyConditionExpression: "PK = :pk",
ExpressionAttributeValues: {
":pk": { S: `USER#${userId}` },
},
})
);

const user = Items.find((i) => i.Type.S === "User");
const orders = Items.filter((i) => i.Type.S === "Order");

return {
user: unmarshall(user),
orders: orders.map(unmarshall),
};
}

// Helper to convert DynamoDB format to plain JS objects
function unmarshall(item) {
const result = {};
for (const [key, value] of Object.entries(item)) {
result[key] = Object.values(value)[0];
}
return result;
}

DynamoDB capacity modes. On-demand mode: pay per request. No capacity planning. Good for unpredictable workloads and new applications. Provisioned mode: you specify read and write capacity units. Cheaper at steady state. Good for predictable workloads. The mistake: using on-demand for a workload that does 10,000 requests per second 24/7. That will cost 5-7x more than provisioned capacity with the right settings.

ElastiCache: Redis, Managed

ElastiCache is managed Redis or Memcached. For Node.js applications, use Redis. Memcached is simpler but Redis has data structures (lists, sets, sorted sets, streams), persistence, and pub/sub that you will eventually need.

What ElastiCache is for: Caching database query results. Session storage. Rate limiting. Real-time leaderboards (Redis sorted sets). Pub/sub for cross-service communication (though SNS + SQS is usually better for production). Job queues (though SQS is purpose-built for this).

What ElastiCache is NOT for: Primary data storage (Redis can persist, but it is not a database). Large objects over 100MB (Redis is in-memory; large values fragment memory). Message queues at scale (use SQS).

Cluster Mode vs non-Cluster Mode. Non-Cluster Mode: one primary node with up to 5 read replicas. All data is on every node. Cluster Mode (Redis Cluster): data is sharded across multiple primary nodes. Each shard holds a subset of the keyspace. Use Cluster Mode when your dataset exceeds the memory of a single node, or when you need write scaling beyond a single primary.

Here is a rate limiter using Redis and the ioredis library — a pattern every production API needs:

import Redis from "ioredis";

const redis = new Redis({
host: process.env.REDIS_ENDPOINT,
port: 6379,
tls: {}, // ElastiCache requires TLS for encryption in transit
retryStrategy: (times) => Math.min(times * 50, 2000),
});

async function rateLimit(userId, limit = 100, windowSeconds = 60) {
const key = `ratelimit:${userId}`;
const now = Date.now();
const windowStart = now - windowSeconds * 1000;

// Use a Redis sorted set: score = timestamp, member = request ID
const requestId = `${now}-${Math.random().toString(36).slice(2)}`;

// Remove expired entries
await redis.zremrangebyscore(key, 0, windowStart);

// Count requests in the current window
const count = await redis.zcard(key);

if (count >= limit) {
return { allowed: false, retryAfter: windowSeconds };
}

// Add current request
await redis.zadd(key, now, requestId);
await redis.expire(key, windowSeconds + 1);

return { allowed: true, remaining: limit - count - 1 };
}

This rate limiter is more accurate than the naive "increment a counter and set TTL" approach because it uses a sliding window. Each request's timestamp is stored individually, and expired entries are cleaned up on each check. At ₹60L+ roles, you are expected to know the difference.


Messaging: How Services Talk to Each Other

SQS: Queues That Do Not Lose Messages

SQS (Simple Queue Service) is a managed message queue. A producer sends messages. A consumer polls for messages, processes them, and deletes them. If a consumer fails to process a message, it becomes visible again after a visibility timeout. This is the foundation of reliable, decoupled architectures.

Standard vs FIFO. Standard queues offer unlimited throughput, at-least-once delivery, and best-effort ordering. Messages may arrive out of order or be delivered more than once. FIFO queues guarantee exactly-once processing and strict ordering, but are limited to 300 messages per second (3,000 with batching).

Use Standard queues for most workloads. The ordering guarantee of FIFO is rarely worth the throughput limitation. If you need ordering, include a sequence number in the message and handle ordering in the consumer. If you need exactly-once processing, make your consumer idempotent — check if the message was already processed before acting on it.

Visibility timeout. When a consumer receives a message, it becomes invisible to other consumers for the visibility timeout period. If the consumer processes and deletes the message within that window, great. If the consumer crashes or the timeout expires, the message becomes visible again and another consumer picks it up.

Set the visibility timeout to the maximum time your consumer needs to process a message, plus a buffer. If your Lambda function has a 30-second timeout, set the visibility timeout to 60 seconds. If your visibility timeout is too short, messages will be processed multiple times. If it is too long, failed messages take longer to be retried.

Dead Letter Queues (DLQs). After a message has been received and returned to the queue N times (the maxReceiveCount), SQS moves it to a dead letter queue. This prevents a poison message from being processed forever. Set up a CloudWatch alarm on the DLQ. If messages appear in the DLQ, something is wrong — a bug in the consumer, a malformed message, a downstream dependency that is down. Investigate immediately.

Here is a Node.js SQS consumer using the AWS SDK v3:

import { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } from "@aws-sdk/client-sqs";

const sqs = new SQSClient({ region: "ap-south-1" });
const QUEUE_URL = process.env.ORDER_PROCESSING_QUEUE_URL;

async function pollQueue() {
while (true) {
try {
const { Messages } = await sqs.send(
new ReceiveMessageCommand({
QueueUrl: QUEUE_URL,
MaxNumberOfMessages: 10,
WaitTimeSeconds: 20, // long polling — reduces empty responses
VisibilityTimeout: 60,
})
);

if (!Messages || Messages.length === 0) continue;

for (const message of Messages) {
try {
const body = JSON.parse(message.Body);
await processOrder(body);

// Delete only after successful processing
await sqs.send(
new DeleteMessageCommand({
QueueUrl: QUEUE_URL,
ReceiptHandle: message.ReceiptHandle,
})
);
} catch (err) {
console.error(`Failed to process ${message.MessageId}:`, err);
// Do NOT delete — message will become visible again after timeout
}
}
} catch (err) {
console.error("Poll error:", err);
await new Promise((r) => setTimeout(r, 1000)); // back off on error
}
}
}

async function processOrder(order) {
// Business logic here
console.log(`Processing order ${order.id}`);
}

Notice WaitTimeSeconds: 20. This is long polling. Without it, SQS returns immediately even if the queue is empty, and you burn CPU and API calls polling nothing. With long polling, SQS waits up to 20 seconds for a message to arrive before returning. This reduces empty responses by 90%+ and cuts your SQS bill.

SNS and EventBridge: Push vs Event Bus

SNS (Simple Notification Service) is a pub/sub service. You create a topic. You publish messages to it. Subscribers (SQS queues, Lambda functions, HTTP endpoints, email, SMS) receive the messages. SNS is push-based: the message is delivered to all subscribers immediately.

Use SNS when you need to fan out a message to multiple consumers. Example: when an order is placed, publish to an SNS topic. One SQS queue (for order fulfillment) and one Lambda function (for sending confirmation email) both subscribe to the topic. Each gets a copy of the message.

EventBridge is SNS on steroids. It is an event bus with content-based routing. Instead of subscribing to a topic, you define rules that match event patterns. An event with { "source": "order-service", "detail-type": "OrderPlaced" } can be routed to different targets based on $.detail.amount > 10000 (high-value orders get special handling).

Use EventBridge when you need: content-based routing, event archival and replay, schema discovery and registry, cross-account event delivery, or integration with SaaS providers (Zendesk, Datadog, PagerDuty).

Use SNS when you need: simple fan-out, SMS/email/push notifications, or the absolute lowest latency (SNS is slightly faster than EventBridge for simple pub/sub).

Here is publishing an event to EventBridge from Node.js:

import { EventBridgeClient, PutEventsCommand } from "@aws-sdk/client-eventbridge";

const eventBridge = new EventBridgeClient({ region: "ap-south-1" });

async function publishOrderPlaced(order) {
await eventBridge.send(
new PutEventsCommand({
Entries: [
{
Source: "order-service",
DetailType: "OrderPlaced",
Detail: JSON.stringify({
orderId: order.id,
userId: order.userId,
amount: order.total,
items: order.items.length,
paymentMethod: order.paymentMethod,
}),
EventBusName: "default",
},
],
})
);
}

The Detail field is free-form JSON. EventBridge does not enforce a schema by default, but you can enable schema discovery to automatically infer and version schemas from events. At ₹60L+ roles, you should know that schema discovery exists and why it matters: it prevents the "what fields does this event have?" guessing game that plagues microservice architectures.


Serverless Patterns: API Gateway + Lambda + DynamoDB

The most common serverless pattern on AWS is API Gateway → Lambda → DynamoDB. It is the "LAMP stack" of the serverless world. You need to know it cold.

API Gateway is the HTTP front door. It receives HTTP requests, validates them, transforms them, and routes them to a backend — usually Lambda, but it can also route to HTTP endpoints, other AWS services, or mock responses.

API Gateway comes in three flavors:

  • REST API — the original. Full-featured. Supports request/response transformation, API keys, usage plans, and caching.
  • HTTP API — cheaper, faster, fewer features. Use this for simple Lambda proxies and when you do not need the advanced features of REST API.
  • WebSocket API — for real-time, bidirectional communication. Use this for chat, live dashboards, and collaborative editing.

For most use cases, HTTP API is the right choice. It is 70% cheaper than REST API and has lower latency. Only use REST API when you need request validation, response transformation, or API key-based rate limiting.

Here is a complete serverless API endpoint: API Gateway (HTTP API) → Lambda → DynamoDB, with proper error handling:

import { DynamoDBClient, GetItemCommand, PutItemCommand } from "@aws-sdk/client-dynamodb";

const dynamo = new DynamoDBClient({ region: process.env.AWS_REGION });
const TABLE = process.env.USERS_TABLE;

export async function handler(event) {
try {
const { httpMethod, pathParameters, body } = event;

switch (httpMethod) {
case "GET":
return await getUser(pathParameters.userId);
case "POST":
return await createUser(JSON.parse(body));
default:
return { statusCode: 405, body: JSON.stringify({ error: "Method not allowed" }) };
}
} catch (err) {
console.error("Handler error:", err);

if (err.name === "ConditionalCheckFailedException") {
return {
statusCode: 409,
body: JSON.stringify({ error: "User already exists" }),
};
}

return {
statusCode: 500,
body: JSON.stringify({ error: "Internal server error" }),
};
}
}

async function getUser(userId) {
const { Item } = await dynamo.send(
new GetItemCommand({
TableName: TABLE,
Key: {
PK: { S: `USER#${userId}` },
SK: { S: `PROFILE#${userId}` },
},
})
);

if (!Item) {
return { statusCode: 404, body: JSON.stringify({ error: "User not found" }) };
}

return {
statusCode: 200,
body: JSON.stringify({
id: Item.PK.S.replace("USER#", ""),
name: Item.name.S,
email: Item.email.S,
createdAt: Item.createdAt.S,
}),
};
}

async function createUser(data) {
const userId = data.id || crypto.randomUUID();
const now = new Date().toISOString();

await dynamo.send(
new PutItemCommand({
TableName: TABLE,
Item: {
PK: { S: `USER#${userId}` },
SK: { S: `PROFILE#${userId}` },
name: { S: data.name },
email: { S: data.email },
createdAt: { S: now },
},
ConditionExpression: "attribute_not_exists(PK)",
})
);

return {
statusCode: 201,
body: JSON.stringify({ id: userId, name: data.name, email: data.email }),
};
}

The ConditionExpression: "attribute_not_exists(PK)" is critical. Without it, two concurrent POST requests with the same ID would silently overwrite each other. With it, the second request gets a ConditionalCheckFailedException, which we handle as a 409 Conflict. This is how you enforce uniqueness in DynamoDB — there is no UNIQUE constraint like in SQL.

The Story of Two Engineers

Let me tell you about two engineers I know in Bangalore. Both had 5 years of experience. Both were Node.js developers. Both interviewed for the same Staff Engineer role at a Series C fintech company offering ₹85 LPA.

The first engineer, call him Vikram, had deep Node.js expertise. He could explain the event loop in detail. He had built complex APIs. He had optimized PostgreSQL queries. When the interviewer asked him to design a payment processing system, he drew a beautiful architecture diagram with microservices, an API gateway, and a database. Then the interviewer asked: "How do you handle a partial AZ failure?" Vikram said: "AWS handles that, right?"

The second engineer, call her Priya, had spent two years deliberately learning infrastructure. She had not just used AWS services — she had broken them and fixed them. When asked the same question, she walked through: multi-AZ RDS with automatic failover, SQS with DLQs for retry logic, idempotency keys to prevent duplicate charges, and CloudWatch alarms that trigger a Lambda to reconcile stuck transactions. She got the offer.

The difference was not coding ability. It was the ability to reason about the system end-to-end, from the user's HTTP request down to the disk where the data is stored. That is what this chapter is about.


IAM: The Glue That Holds Everything Together (or Burns It Down)

IAM (Identity and Access Management) controls who can do what in your AWS account. It is the most important service you will never see on a resume. Nobody lists "IAM expert" as a skill. But every major AWS security incident — every S3 bucket left public, every leaked credential, every cryptojacking attack — traces back to an IAM misconfiguration.

The IAM model, in one paragraph. You create policies (JSON documents that say "allow this action on this resource"). You attach policies to roles or users. Roles are assumed by AWS services (Lambda, EC2, ECS tasks) or by federated identities. Users are for humans (but use SSO, not IAM users, for human access). Resources have resource-based policies (S3 bucket policies, SQS queue policies) that control cross-account access.

The principle of least privilege. Every role should have exactly the permissions it needs and nothing more. Not s3:* on *. Not dynamodb:* on *. Specific actions on specific resources.

Here is a well-scoped IAM policy for a Lambda function that reads from one S3 bucket and writes to one DynamoDB table:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "arn:aws:s3:::myapp-uploads/*"
},
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:ap-south-1:123456789:table/MetadataTable"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:ap-south-1:123456789:*"
}
]
}

Notice: the S3 permission is scoped to a specific bucket (myapp-uploads) and specific objects within it (/*). The DynamoDB permission is scoped to a specific table. The CloudWatch Logs permission is required for Lambda to write logs — without it, your function runs but produces no logs, and you will spend an hour debugging why.

IAM roles for ECS tasks. This is where most engineers get confused. An ECS task has two roles:

  1. Execution Role — what ECS itself needs: pull images from ECR, write logs to CloudWatch, read secrets from Secrets Manager.
  2. Task Role — what your application code needs: read from S3, write to DynamoDB, publish to SNS.

These are separate for a reason. The execution role is used by the ECS agent, not your code. If you put S3 permissions in the execution role, your application cannot access S3 — the ECS agent does not pass those credentials to your container. This is a common source of "it works on my machine but not in ECS" bugs.

IAM conditions. Policies can include conditions that restrict when a permission applies. The most useful conditions:

  • aws:SourceIp — only allow from specific IP ranges (your office VPN)
  • aws:RequestedRegion — only allow actions in specific regions
  • aws:MultiFactorAuthPresent — require MFA for sensitive actions
  • s3:x-amz-server-side-encryption — require SSE for S3 uploads

Here is a policy that enforces SSE-S3 encryption on all uploads:

{
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::myapp-uploads/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "AES256"
}
}
}

This is a "deny unless" pattern. It denies PutObject unless the request includes the SSE header. This is how you enforce security invariants: not by asking developers to remember, but by making the platform reject non-compliant requests.


Putting It All Together: A Production Architecture

Let us walk through a real architecture that uses all twelve services. This is the kind of system you should be able to design and explain in a Staff Engineer interview.

The system: A food delivery platform serving 500,000 orders per day across 10 Indian cities. Peak load: 2,000 orders per minute during lunch (12:30-1:30 PM).

The architecture:

  1. API Gateway (HTTP API) receives all client requests. Two stages: prod and staging. Custom domain with ACM certificate. WAF (Web Application Firewall) attached for rate limiting and SQL injection protection.

  2. ALB routes traffic to the backend microservices running on ECS Fargate. Path-based routing: /api/orders/* → order service, /api/restaurants/* → restaurant service, /api/users/* → user service.

  3. ECS Fargate runs the Node.js microservices. Each service has its own task definition, task role, and auto-scaling policy. Services scale based on CPU utilization (target 70%) and request count per target (target 1,000 requests/minute/task).

  4. VPC with three AZs, three public subnets (for ALB and NAT Gateways), and three private subnets (for ECS tasks, RDS, ElastiCache, Lambda with VPC config). NAT Gateway in each public subnet.

  5. RDS (PostgreSQL, Multi-AZ) stores user profiles, restaurant menus, and order history. Primary in ap-south-1a, standby in ap-south-1b. Read replica in ap-south-1c for analytics queries. RDS Proxy in front for connection pooling.

  6. DynamoDB stores the active orders table (single-table design with order status as GSI sort key) and the driver location table (geo-hashed partition key for efficient proximity queries). On-demand capacity mode to handle lunch spikes.

  7. ElastiCache (Redis, Cluster Mode) caches restaurant menus (most-read, rarely-changed data), stores user sessions, and powers the real-time order tracking (Redis Pub/Sub for driver location updates).

  8. SQS decouples order placement from order processing. When an order is placed, the API publishes to an SQS queue. A separate ECS service consumes the queue and handles: payment verification, restaurant notification, driver assignment. DLQ configured with CloudWatch alarm.

  9. SNS fans out order status changes. When an order status changes (confirmed → preparing → picked up → delivered), the order service publishes to an SNS topic. Subscribers: push notification Lambda (sends FCM/APNS to user's phone), email Lambda (sends confirmation email), analytics Lambda (writes to the data warehouse).

  10. EventBridge handles cross-service events. Restaurant service emits RestaurantClosed events. Order service subscribes to reject pending orders for that restaurant. Driver service emits DriverAvailable events. Order service subscribes to assign waiting orders.

  11. Lambda handles event-driven, spiky workloads: image resizing (triggered by S3 uploads), push notifications (triggered by SNS), scheduled cleanup jobs (triggered by EventBridge scheduled rules), and the order reconciliation job (triggered by CloudWatch alarm when DLQ depth > 0).

  12. S3 stores restaurant images, user profile photos, CloudTrail logs, and database snapshots. Lifecycle policies move objects to Standard-IA after 30 days and Glacier after 90 days. Versioning enabled. SSE-S3 encryption enforced via bucket policy.

  13. IAM ties it all together. Each ECS service has a task role scoped to exactly the resources it needs. Lambda functions have execution roles with least privilege. No service shares credentials. No service has * permissions.

This architecture handles 500,000 orders per day with sub-200ms API latency, survives a single AZ failure without data loss, and costs approximately ₹4.5 lakh/month in AWS infrastructure. That is ₹54 lakh per year — less than the salary of one Staff Engineer, for a system that serves 15 million orders per month.

What You Do Not Need to Know (Yet)

A chapter on cloud engineering would be incomplete without telling you what to ignore. The AWS console lists over 200 services. You do not need to know most of them. Here is what you can safely skip at the Staff Engineer level:

  • Kinesis — unless you are building a real-time analytics pipeline processing terabytes per hour, SQS + Lambda handles most streaming use cases.
  • Step Functions — useful for complex workflows, but you can model most workflows with SQS + Lambda + a state machine in code.
  • AppSync — GraphQL as a service. If you need GraphQL, run Apollo Server on ECS. AppSync locks you into AWS-specific resolver patterns.
  • CodeBuild/CodePipeline/CodeDeploy — AWS's CI/CD tools. They work, but GitHub Actions and GitLab CI are more widely used and more portable.
  • CloudFormation — learn Terraform instead. It is cloud-agnostic and has a larger community. The concepts transfer.
  • Redshift — unless you are a data engineer, you do not need a petabyte-scale data warehouse.
  • EMR — unless you are running Hadoop/Spark clusters, which is a data engineering specialization.

The pattern: if a service is domain-specific (data engineering, ML, IoT, blockchain), you do not need it for a Staff Engineer role in application engineering. Learn it when your project demands it.

The Practice: Design Your Own Architecture

Here is your assignment. Do not skip this. Reading about cloud architecture is like reading about swimming — you do not learn until you get in the water.

Scenario: You are the first infrastructure hire at a Bangalore-based healthtech startup. The product is a telemedicine platform: patients book video consultations with doctors, doctors write prescriptions, patients order medicines. Current state: a monolithic Node.js app on a single EC2 instance, PostgreSQL on the same instance, no redundancy, no monitoring. The CTO wants you to design the production architecture for launch in 3 months.

Constraints:

  • Target: 10,000 consultations per day at launch, scaling to 100,000 within 12 months
  • Compliance: patient data must be encrypted at rest and in transit, must stay within India (ap-south-1 only)
  • Budget: ₹2 lakh/month for infrastructure at launch
  • Video calls: use a third-party service (TokBox/Agora) — do not build this yourself
  • Prescriptions: must be stored for 7 years (regulatory requirement)

Your task: Draw the architecture diagram. List every AWS service you would use. For each service, explain: why you chose it, what configuration options matter, and what the cost implications are. Write the IAM policy for the prescription storage service. Write the Node.js code for the appointment booking Lambda function.

Do this on paper first. Then build it in the AWS free tier. Break it. Fix it. This is how you learn.


The Consequence

Here is what happens if you skip this chapter and hope infrastructure stays "someone else's problem."

You will spend the next five years writing excellent Node.js code. You will ship features. You will get promoted to Senior Engineer. And then you will hit the wall. The ₹60 lakh roles will ask you about VPC design, about DynamoDB access patterns, about SQS failure modes. You will not have answers. You will be a senior developer who deploys to someone else's infrastructure — and that someone else will be the Staff Engineer who got the role you wanted.

The twelve services in this chapter are not optional knowledge for a ₹1 crore engineer. They are the table stakes. Learn them. Build with them. Break them. Own them.

Because in the room where compensation decisions are made, the engineer who can debug a production outage at 3 AM without calling AWS Support is worth exactly twice the engineer who cannot.