AWS Reference Architecture for Modern Cloud Applications: A Practical Blueprint
This page documents a recommended reference architecture for a modern web application on AWS. It is written for developers building their first production AWS deployment, or architects who want a vetted baseline rather than starting from scratch each time. It covers ten layers, from edge delivery to CI/CD, with service recommendations, trade-off explanations, and the mistakes that slow teams down.
The default recommendation is a ten-layer stack: CloudFront at the edge, S3 or App Runner for the frontend, Cognito for authentication, API Gateway and Lambda for the API, RDS as the primary database, DynamoDB for key-value patterns, S3 for file uploads, Secrets Manager for credentials, CloudWatch and X-Ray for observability, and Terraform with GitHub Actions for deployment.
This architecture is overbuilt for a small MVP. If you are building a prototype, a static marketing site, or a low-traffic internal tool, the section on when not to use this architecture will point you toward a simpler path.
If this page feels like a lot: jump straight to Recommended baseline architecture for a specific service list, then come back to read the reasoning behind each choice.
Simple explanation
A modern cloud web application works in distinct layers. Here is what each one does in plain terms:
- How traffic reaches the app: CloudFront delivers static files from an edge location close to the user. Route 53 resolves the domain. Cached assets are served instantly; uncached requests go to the origin.
- How users authenticate: Cognito handles sign-up, login, and multi-factor auth. It issues signed JWT tokens your API validates on every request, with no custom auth code required.
- Where code runs: API Gateway receives HTTP requests from the frontend. Lambda executes your backend logic: stateless, serverless, and auto-scaling from zero.
- Where data lives: RDS holds relational data (users, orders, products). DynamoDB handles key-value data (sessions, counters, flags). S3 stores files and media.
- How you observe and deploy: CloudWatch collects logs and metrics. X-Ray traces requests across services. Terraform defines infrastructure as code; GitHub Actions deploys application changes on every merge.
Architecture at a glance
| Layer | Recommended service | Why it is used | Common alternative | When to switch |
|---|---|---|---|---|
| Edge / CDN | CloudFront | Global caching, free TLS via ACM, Shield DDoS protection | Cloudflare | Team already uses Cloudflare and wants to consolidate tooling |
| Frontend hosting | S3 + CloudFront | Zero servers, low cost, simple ops for static apps | App Runner | Framework requires server-rendering (Next.js SSR, Remix, Nuxt) |
| Authentication | Cognito | Managed user pools, JWT issuance, social login, SAML | Auth0 | Enterprise CIAM features or pricing at scale require a dedicated IdP |
| API layer | API Gateway + Lambda | Auto-scales, pay per request, no container management | ALB + ECS | Long-running requests, persistent connections, or always-warm compute needed |
| Primary database | RDS (PostgreSQL) | Relational structure, ACID transactions, complex queries | Aurora PostgreSQL | High read throughput or serverless scaling mode justifies Aurora’s higher cost |
| Key-value / sessions | DynamoDB | Single-digit ms latency, scales on-demand | ElastiCache (Redis) | Pub/sub, sorted sets, or Lua scripting required |
| File storage | S3 | Low cost, durable, presigned URLs, versioning | EFS | Lambda or containers need a shared mounted filesystem |
| Secrets | Secrets Manager | Encrypted storage, automatic rotation, audit trail | SSM Parameter Store | Non-secret config strings only (SSM is cheaper for plain values) |
| Observability | CloudWatch + X-Ray | Native integration; logs, metrics, and traces in one place | Datadog / Grafana | Advanced dashboards or cross-cloud observability required |
| CI/CD and IaC | GitHub Actions + Terraform | Open ecosystem, portable, widely documented | CodePipeline + CDK | AWS-native teams or org-wide pipeline governance requirements |
How it works: end-to-end request flow
Here is what happens when a user makes an authenticated API request, from browser to response:
- The user opens the app. The browser resolves the domain via Route 53 to a CloudFront distribution.
- CloudFront checks its cache. If the HTML, JS bundle, or CSS file is cached at the edge, it returns immediately with no origin request needed.
- For a cache miss, CloudFront fetches the asset from the origin: S3 for a static SPA, or App Runner for a server-rendered app.
- When the frontend needs data, it sends an API call. CloudFront routes
/api/*requests to API Gateway. - API Gateway passes the request to its Cognito authorizer, which validates the JWT in the
Authorizationheader: signature, expiry, and audience. Invalid tokens are rejected with a 401 before Lambda is invoked. - For a valid token, API Gateway invokes the Lambda function.
- Lambda retrieves database credentials from Secrets Manager on cold start, then caches them in memory for subsequent invocations on the same instance.
- Lambda queries RDS for relational data or DynamoDB for session and key-value data. For file access, Lambda generates a presigned S3 URL or reads directly from S3.
- Lambda returns a response. API Gateway formats it and returns it to CloudFront, which returns it to the browser.
- Throughout steps 4–9, CloudWatch Logs captures output from Lambda and API Gateway. X-Ray records a distributed trace with API Gateway entry time, Lambda execution duration, database query time, and any downstream calls.
Browser → Route 53 → CloudFront
├── [static asset, cache hit] → cached response
├── [static asset, cache miss] → S3 or App Runner
└── [/api/* request] → API Gateway
├── Cognito authorizer (validate JWT)
└── Lambda
├── Secrets Manager (credentials)
├── RDS (relational queries)
├── DynamoDB (sessions / key-value)
└── S3 (presigned URL or direct read)
CloudWatch Logs + X-Ray trace all layersThink of CloudFront as a chain of local library branches. Most visitors get what they need from the nearest branch without driving to the central library. When the branch does not have a book, it orders it from the centre and keeps a copy so the next person does not have to wait. Your origin server is the central library. Most of your users never reach it.
Recommended baseline architecture
For a new production web application on AWS, start with these specific choices:
- Edge: CloudFront distribution with ACM certificate and origin access control for the S3 bucket
- Frontend: S3 bucket (static SPA or statically generated site) served through CloudFront
- Auth: Cognito user pool with a Cognito authorizer on API Gateway
- API: API Gateway (HTTP API) with Lambda functions grouped by resource or domain
- Primary database: RDS PostgreSQL,
db.t3.medium, Multi-AZ enabled, automated backups with 7-day retention - Key-value store: DynamoDB on-demand mode, but only when you have a specific key-value access pattern that justifies it
- File storage: S3 bucket with presigned URLs for direct browser uploads; no public access
- Secrets: Secrets Manager for RDS credentials, retrieved at Lambda cold start and not stored in environment variables
- Observability: CloudWatch alarms on Lambda error rate, API Gateway 5xx rate, and RDS CPU; X-Ray enabled on API Gateway and Lambda; log retention set to 30 days on all log groups
- CI/CD and IaC: Terraform for all resources; GitHub Actions for application deployments;
terraform planreviewed on pull request,terraform applyon merge to main
This is not the cheapest setup possible, but it is a defensible production baseline that handles growth without forcing a major rearchitect later.
Core layers
Edge and CDN
CloudFront is a CDN (Content Delivery Network) with 450+ Points of Presence worldwide. When a user requests your app from Tokyo, CloudFront serves the response from a nearby edge location rather than your origin in us-east-1. For static assets (JavaScript bundles, CSS, images), this latency difference is significant.
CloudFront also does more than cache:
- Free TLS: ACM certificates integrate directly with CloudFront at no certificate cost
- DDoS protection: AWS Shield Standard is included, absorbing volumetric attacks before they reach your origin
- Security headers: Response headers policies add HSTS, CSP, and X-Frame-Options without touching application code
- Origin access control: Locks your S3 bucket so only CloudFront can read from it; direct S3 URLs return 403
A single CloudFront distribution can serve both origins: S3 for static assets on /* and API Gateway for requests on /api/*. Your application appears as a single domain to users.
One common mistake: leaving the S3 bucket public and treating CloudFront as optional. Set up origin access control at the start and block all direct S3 public access.
Frontend hosting
The right choice depends on your rendering model.
S3 + CloudFront handles Single Page Applications and statically generated sites (React, Vue, Angular, Next.js with static export). Build to static files, upload to S3, serve via CloudFront. No servers, no scaling configuration, and cost is pennies for most apps.
If you are not sure which rendering model to use, start here. Most apps behind a login do not need server-side rendering, and the operational difference between static and SSR is larger than it looks at first.
App Runner is the option when your framework renders pages on the server: Next.js SSR, Remix, Nuxt, SvelteKit with server routes. App Runner takes a container image, handles scaling and load balancing automatically, and provides an HTTPS endpoint with no ECS or Kubernetes required. CloudFront can still sit in front for caching static assets.
For applications that require login, server-side rendering rarely provides meaningful SEO benefit. Prefer S3 + CloudFront and build in SPA mode unless you have a concrete case for SSR.
Authentication
Building custom authentication is almost never worth the effort or the security risk. Cognito is a managed service that handles user registration, login, password reset, MFA, and JWT issuance.
The authentication flow:
- User submits credentials to the Cognito-hosted UI or a custom form calling the Cognito API
- Cognito validates credentials, triggers MFA if configured, and issues an ID token and access token (both JWTs)
- The access token travels with every API request in the
Authorizationheader - API Gateway’s Cognito authorizer validates the token signature and expiry; invalid tokens are rejected before Lambda is invoked
- Lambda receives the request with user identity already confirmed, with no auth code needed in business logic
A JWT is like a signed badge from a security desk. The badge says “this is Alice, she has editor access, and it expires at 5pm.” Every door in the building reads the badge directly without calling the security desk again. Cognito is the security desk that prints and signs the badges. API Gateway is every door that checks them.
Cognito also supports Google, Apple, and Facebook social login, and enterprise SAML/OIDC for B2B SSO, all configured without writing custom SAML parsing code.
Each user pool lives in one AWS region. For multi-region deployments, you need a strategy: either one authoritative pool with cross-region API calls, or active-active replication. For most apps, a single-region pool is sufficient.
API and compute
API Gateway acts as the managed front door for your backend. It handles routing, Cognito token validation, rate limiting, and request/response transformation. It fits naturally into service-oriented and microservices patterns.
Lambda functions are the handlers. Each endpoint or group of endpoints maps to a Lambda function. Lambda scales from zero to thousands of concurrent executions automatically. You pay for execution time only, with no cost when idle.
Lambda is stateless by design: each invocation runs in a clean environment. Long-lived state belongs in a dedicated session or cache layer, not in Lambda’s in-process memory.
To connect Lambda to RDS in a private subnet, Lambda needs VPC configuration: subnets and a security group allowed inbound by the RDS security group. Lambda functions that only call DynamoDB, S3, SQS, or other AWS service APIs do not need to be inside a VPC at all.
Adding VPC config adds 100–500ms to cold starts and requires a NAT Gateway for internet access (roughly $32/month per AZ, per region). Only configure VPC when Lambda genuinely needs to reach a private resource, like RDS in a private subnet.
For event-driven patterns such as processing SQS queues or reacting to S3 uploads, Lambda attaches to those event sources directly without API Gateway in the path.
A minimal Terraform example for API Gateway + Lambda with a Cognito authorizer:
resource "aws_api_gateway_rest_api" "main" {
name = "myapp-api"
}
resource "aws_api_gateway_authorizer" "cognito" {
name = "cognito-auth"
rest_api_id = aws_api_gateway_rest_api.main.id
type = "COGNITO_USER_POOLS"
provider_arns = [aws_cognito_user_pool.main.arn]
}
resource "aws_lambda_function" "api_handler" {
function_name = "myapp-handler"
runtime = "nodejs20.x"
handler = "index.handler"
role = aws_iam_role.lambda_exec.arn
filename = "function.zip"
environment {
variables = {
DB_SECRET_ARN = aws_secretsmanager_secret.db.arn
}
}
}Data layer
This architecture uses two database services for different workloads. Not because two is always necessary, but because their access patterns usually differ enough to justify separate tools.
RDS (PostgreSQL or MySQL) stores the primary application data: user accounts, orders, products, content — anything with relational structure, foreign keys, and queries that join across tables. Deploy RDS in a private subnet with Multi-AZ enabled. Multi-AZ keeps a standby replica in a second Availability Zone and fails over automatically if the primary instance goes down. Enable automated backups with a 7-day retention window. For high availability and read scaling, read replicas can distribute read traffic when write load is not the bottleneck.
DynamoDB stores high-throughput key-value data: session tokens, rate limiting counters, feature flags, and append-only event logs. On-demand mode removes capacity planning. DynamoDB is well-suited for these patterns but unsuitable for ad-hoc queries, joins, or complex reporting.
The most common starting mistake: choosing DynamoDB as the primary application database because “it scales better.” Without a clear partition key strategy and known access patterns, you will find yourself unable to retrieve data without expensive full table scans. See the RDS vs DynamoDB comparison before committing to DynamoDB as your primary store.
RDS is like a spreadsheet with enforced columns, formulas, and the ability to answer questions like “show me all orders from California customers who spent over $100 last month.” DynamoDB is a set of labeled filing drawers: you pull out exactly the drawer you labeled, instantly, but browsing the whole cabinet or asking cross-drawer questions is slow and expensive. Choose based on whether you need to query flexibly or just retrieve predictably.
File storage
S3 handles binary file storage: user uploads, profile images, document exports, and media assets. The recommended upload pattern is presigned URLs: Lambda generates a time-limited signed URL, the browser uploads directly to S3, and an S3 event notification triggers a Lambda function when the upload completes.
This keeps large binary files off your API tier, reduces Lambda execution time, and avoids API Gateway payload limits. See the S3 signed URLs guide for how to generate and validate them.
Do not set the uploads bucket to public access. Use bucket policies and presigned URLs for controlled access. Enable Block Public Access on the bucket.
Secrets management
Database credentials, third-party API keys, and OAuth secrets must not live in Lambda environment variables or config files checked into source control.
The Lambda console shows environment variables in plaintext. They can also surface in debug output, error tracking tools, and support transcripts. Use Secrets Manager instead and retrieve them at cold start.
Secrets Manager stores credentials encrypted, provides an audit trail via CloudTrail, and rotates RDS credentials automatically on a configurable schedule. The Lambda execution role gets secretsmanager:GetSecretValue on the specific secret ARN, nothing broader.
The access pattern: Lambda retrieves the secret on cold start, caches it in memory, and reuses the cached value for subsequent invocations on the same instance. If many Lambda functions connect to RDS simultaneously, add RDS Proxy to pool and reuse database connections to prevent the “too many connections” error at scale.
Observability
CloudWatch is the primary observability tool. Every AWS service emits metrics to CloudWatch automatically: Lambda invocation counts and error rates, API Gateway 4xx and 5xx rates, RDS CPU and active connections.
Set alarms on the signals that matter for production:
- Lambda error rate above 1% → alert
- API Gateway 5xx rate above 0.5% → alert
- RDS CPU above 80% for more than 5 minutes → alert
- Dead-letter queue message count above 0 → alert immediately
Every Lambda function and API Gateway stage creates a CloudWatch log group with infinite retention. At scale this becomes a real cost: $0.03/GB/month. Set retention to 30 days for application logs and 90 days for security-relevant logs on every group, during initial setup and not after your first surprising AWS bill.
AWS X-Ray provides distributed tracing. When a request enters API Gateway, passes through Lambda, queries RDS, and publishes to SQS, X-Ray builds a trace showing each step, its duration, and any errors. Enable X-Ray on API Gateway and Lambda with a few lines of configuration. It is the fastest way to find which downstream dependency is causing latency or failures.
CI/CD and infrastructure as code
Two deployment pipelines serve different purposes.
Application code: A merge to main triggers a GitHub Actions workflow that runs tests, packages the Lambda deployment artifact or container image, and deploys. Lambda aliases and weighted routing allow gradual traffic shifting from old to new versions, with an immediate rollback path if error rates rise.
Infrastructure: All AWS resources (VPCs, subnets, security groups, RDS instances, Lambda functions, IAM roles) are defined in Terraform files checked into version control. The CI pipeline runs terraform plan on pull requests, with the plan output reviewed as part of code review, then terraform apply on merge to main. This prevents manual console changes from silently drifting away from the defined state.
Every change to the system, whether code or infrastructure, goes through review, automated testing, and a controlled deployment before reaching production.
When to use this architecture
This architecture fits well when:
- SaaS application with user accounts: Multiple users, roles, and organizations with data isolated per account. Cognito handles identity; RDS handles the relational data model; the API layer scales with traffic.
- API-backed web product: A React or Vue frontend consuming a JSON API. API Gateway + Lambda handles variable traffic without per-request capacity planning.
- Internal business application: A tool used by employees. The same stack applies; you can restrict Cognito to your corporate identity provider via SAML for SSO.
- Startup app growing toward production maturity: Start with a simplified version: skip DynamoDB until you have a session or cache need, skip X-Ray until you have something worth tracing. Add the remaining layers as product needs and team capacity grow.
When not to use this architecture
Simplify or choose a different approach when:
- MVP or prototype: A single EC2 instance with RDS and S3 deploys faster and is cheaper to operate before you have real users.
- Static marketing site: No database, no Lambda, no Cognito needed. S3 + CloudFront is the entire stack.
- Low-complexity internal tool: A small internal CRUD app for 10–20 employees is easier to operate as a single container on ECS or a small EC2 instance, not a distributed serverless API.
- Streaming or long-running workloads: Lambda has a 15-minute max execution time. Real-time streaming, video processing pipelines, and WebSocket servers with persistent connection state need ECS, EC2, or a managed streaming service.
- Heavy batch processing: Lambda is unsuitable for jobs that run for hours or require GPU access. AWS Batch or ECS fits these workloads better.
- Team already committed to containers: If the team has Kubernetes or ECS expertise, that operational knowledge is worth preserving. Do not switch to Lambda just because this reference architecture defaults to it.
Key decisions and comparisons
S3 + CloudFront vs App Runner
| S3 + CloudFront | App Runner | |
|---|---|---|
| Rendering model | Static files only (SPA, SSG) | Server-rendered (SSR) |
| Cost | Very low (storage + transfer) | Per vCPU-hour; scales to zero after inactivity |
| Operational overhead | Near zero | Low (managed scaling, no ECS or Kubernetes) |
| Cold starts | None | Container startup (~1–10s depending on image) |
| Use when | React/Vue SPA, Next.js with static export | Next.js SSR, Remix, Nuxt, SvelteKit server routes |
Choose S3 + CloudFront when your framework can produce static files. Most login-gated apps do not need SSR. The near-zero operational cost and simplicity are hard to beat.
Choose App Runner when server-rendering is a genuine requirement: real-time personalization at the HTML level, or a framework that does not support static export.
API Gateway + Lambda vs ALB + ECS
| API Gateway + Lambda | ALB + ECS | |
|---|---|---|
| Scales to zero | Yes | No (minimum task count) |
| Max execution time | 15 minutes per invocation | No limit |
| Cold start latency | 100ms–3s depending on runtime | Container startup, amortized across warm instances |
| State model | Stateless per invocation | Stateful possible (in-process memory) |
| Operational complexity | Low | Moderate (container builds, task sizing, cluster config) |
Choose API Gateway + Lambda for most new REST or HTTP APIs. Automatic scaling, zero idle cost, and low operational overhead outweigh the cold start trade-off for standard request/response patterns.
Choose ALB + ECS when Lambda’s timeout limit is a real constraint, when requests need persistent in-process state, or when the team already maintains containerized services. See Choosing Between Lambda, ECS, and EC2 for a fuller treatment.
RDS vs DynamoDB
| RDS (PostgreSQL) | DynamoDB | |
|---|---|---|
| Query model | SQL: joins, aggregations, ad-hoc queries | Key-value / document, access by primary key or GSI only |
| Schema | Structured, enforced | Flexible per item |
| Scaling model | Vertical + read replicas | Horizontal, on-demand or provisioned |
| Latency | ~1–5ms for simple queries | Single-digit ms at any throughput |
| Best for | Relational data, complex queries | High-throughput key-value, sessions, counters |
Choose RDS as the primary application database for most web apps. Start with db.t3.medium and resize based on CloudWatch metrics.
Choose DynamoDB for specific high-throughput key-value patterns: session tokens, rate limiting, feature flags. Do not default to DynamoDB without mapping your access patterns first. The RDS vs DynamoDB comparison covers the trade-offs in depth.
Common mistakes
- Overbuilding on day one. This architecture represents a mature production setup. A new application does not need all ten layers from the start. Over-engineering early delays shipping and adds operational surface area with no current benefit. Build toward this incrementally.
- Lambda in a VPC without a reason. Placing Lambda inside a VPC adds cold start latency and requires a NAT Gateway for internet access. Only put Lambda in a VPC when it needs to reach a VPC-only resource (typically RDS in a private subnet). Lambda that calls DynamoDB, S3, SQS, or other AWS service APIs does not need VPC configuration.
- Not setting CloudWatch log retention. Every Lambda function, API Gateway stage, and ECS task creates CloudWatch log groups with infinite retention by default. Set retention to 30–90 days on every log group from the start. Unmanaged log accumulation costs money and is easy to miss until it appears on a bill.
- Storing secrets in Lambda environment variables. The Lambda console shows environment variables in plaintext. Store credentials in Secrets Manager and retrieve them at Lambda cold start. Scope the Lambda execution role to
secretsmanager:GetSecretValueon the specific secret ARN only, not on all secrets. - Choosing DynamoDB without clear access patterns. DynamoDB requires a partition key and sort key defined upfront. Without a clear data model, you will find yourself unable to query the data you need without full table scans. Read the RDS vs DynamoDB comparison before committing to DynamoDB as your primary store.
- Skipping HA configuration until something breaks. Multi-AZ RDS, Lambda reserved concurrency, and API Gateway throttling are not hard to configure, but they are easy to skip in early development. Set up Multi-AZ and baseline alarms during initial deployment. See Designing Highly Available Systems in AWS for the full checklist.
Frequently asked questions
Do I need all of these services for a small application?
No. This reference architecture is a production target, not a starting checklist. A small application might begin with EC2 + RDS + S3, then add CloudFront, Cognito, and full observability when the product outgrows the simpler setup. Start with what solves your current problems.
When should I choose App Runner instead of S3 + CloudFront for my frontend?
Choose App Runner when your frontend framework renders pages on the server: Next.js in SSR mode, Remix, Nuxt, or SvelteKit with server routes. S3 + CloudFront handles static files only. If you can build your app to static files, S3 + CloudFront is simpler and cheaper.
When should I replace Lambda with ECS for my API?
Switch to ECS when Lambda functions hit the 15-minute timeout, when the workload needs persistent in-memory state between requests, or when cold start latency is a consistent user-facing problem. For most new APIs, Lambda is the better starting point: it scales to zero, costs nothing idle, and requires no container orchestration.
Do I need both RDS and DynamoDB?
Usually not on day one. RDS handles most application data well: relational structure, transactions, complex queries. Add DynamoDB when a specific access pattern justifies it, such as session tokens, rate limiting counters, or feature flags. Do not default to DynamoDB without mapping out your access patterns first.
When do I actually need multi-region?
Multi-region is justified when your SLA requires 99.99%+ availability, when regulations require data residency in specific regions, or when you have a large user base spread across continents with hard latency requirements. For most applications, a single region with Multi-AZ RDS and cross-region S3 backups is sufficient.