Most AWS diagrams I find online fall into one of two groups. Some are simple enough to fit on a slide but skip the details that break in production. Others are complete reference architectures with forty icons and no clear starting point.
I wanted something in between, so I built a catalog of 12 AWS reference architectures. It starts with a static site and grows one concern at a time until it reaches multi-region disaster recovery. Each diagram is a single self-contained HTML page with the services involved, the numbered information flow, comments on the parts that are easy to misread, and one practical note to consider before you build it.
- 🌐 Live catalog: https://josesaid.github.io/aws-architecture-diagrams/
- 💻 Source: https://github.com/josesaid/aws-architecture-diagrams
This article walks through the 12 architectures, explains why each one exists, and shows two of them as Java infrastructure code with the AWS CDK.
The problem: knowing which services go together
AWS has more than 200 services. Learning what each one does is the easy part. The hard part is knowing which services go together for a specific problem and in what order a request moves through them.
That gap shows up in familiar ways:
- A static site ends up with a public S3 bucket because nobody explained Origin Access Control.
- An SQS consumer processes the same message twice because the visibility timeout is shorter than the Lambda timeout.
- A RAG assistant gives poor answers because the embeddings at query time come from a different model than the ones used during ingestion.
- A disaster recovery plan exists on paper, but nobody defined the RTO and RPO, so nobody can say whether it is good enough.
None of these are exotic. They are what happens when you know the services but not how they fit together. Each diagram in the catalog targets one of these combinations and calls out the detail that tends to go wrong.
How each diagram is structured
Every page follows the same layout so you can compare them quickly:
- Summary: one paragraph describing what the architecture solves.
- Services: the AWS services involved, as chips.
- Diagram: the services, with numbered arrows for user traffic and dashed lines for configuration (for example, ACM providing a certificate to CloudFront).
- Comments: what the diagram simplifies and why.
- Information flow: the numbered steps a request follows, one service at a time.
- Note to consider: the operational detail that usually causes trouble.
The pages are plain HTML and CSS with no build step and no framework, so they load instantly and can be hosted anywhere. The catalog itself runs on GitHub Pages.
The 12 architectures
1. Static HTML site
Route 53 → CloudFront (ACM certificate) → private S3 bucket with OAC.
The baseline. Route 53 resolves the domain, CloudFront serves the site over HTTPS, and S3 stores the files in a bucket that only CloudFront can read. Users never reach the bucket directly.
Note to consider: if users see old content after a deploy, CloudFront is serving cached objects. Create an invalidation or wait for the TTL to expire.
2. Layered security for a static site
Diagram 1 + AWS WAF + AWS Shield + AWS KMS.
The same site with protection layers. WAF evaluates every request against managed rules (OWASP, bots, rate limits), Shield Standard mitigates DDoS attacks at no extra cost, OAC keeps the bucket private, and KMS encrypts the objects at rest.
Note to consider: WAF is billed per Web ACL, per rule and per million requests. Start with managed rules and a rate limit, and review the logs before adding custom rules.
3. Automated deployment (CI/CD)
Source bucket → CodePipeline → CodeBuild → site bucket + CloudFront invalidation.
A code change triggers the pipeline. CodeBuild installs dependencies, runs the tests and builds the site, then publishes it and invalidates the cache. Using two separate buckets, one for the source and one for the published site, prevents uncompiled code from going live.
Note to consider: if the tests fail, the previous site stays live. Turn on S3 versioning so you can roll back quickly.
4. Dynamic site with ALB, ECS and RDS
CloudFront → ALB → ECS Fargate (private subnets) → RDS, with static files from S3.
The first architecture with server-side logic. CloudFront splits the traffic: dynamic requests go to the load balancer and static assets come from S3. Only the ALB is exposed, and RDS has no public access.
Note to consider: the RDS security group should accept traffic only from the ECS security group. Multi-AZ keeps the database available when an availability zone fails.
5. Serverless site with login and API
Amplify Hosting + Cognito + API Gateway + Lambda + DynamoDB.
Cognito issues JWT tokens, API Gateway validates them with a Cognito authorizer, and Lambda only runs business logic on top of DynamoDB. No servers to patch.
Note to consider: define Cognito user groups from the start so you can control permissions without changing the authentication code later.
6. Asynchronous processing with SQS
API Gateway → SQS (+ dead-letter queue) → Lambda → DynamoDB, with notifications via SNS.
Requests are accepted fast and processed later. If Lambda is slow or fails, messages wait in the queue instead of being lost. Messages that keep failing go to a dead-letter queue.
Note to consider: the SQS visibility timeout must be longer than the maximum Lambda execution time. Otherwise the same message gets processed twice. The Java section below shows how to set it.
7. Events and orchestration with Step Functions
API Gateway → EventBridge → Step Functions → Lambda (validate, charge) → Aurora Serverless v2.
EventBridge decouples the producer of an event from its consumers. Step Functions stores the state of each execution and retries failed steps with backoff, so the order workflow stays easy to audit.
Note to consider: Standard workflows suit long-running, auditable processes. Express workflows cost less for high volumes of short executions, but they do not keep execution history the same way.
8. Real time with AppSync and DynamoDB Streams
AppSync (GraphQL) → DynamoDB → Streams → Lambda → AppSync subscriptions.
Changes reach every connected client without polling. AppSync manages the WebSocket subscriptions for you.
Note to consider: subscriptions are billed per connection and per message. Use subscription filters so each client receives only the events it needs.
9. Data and analytics on a data lake
Applications → Data Firehose → S3 (data lake) → Glue Catalog → Athena → QuickSight.
Firehose batches the records into S3, Glue keeps the table catalog, Athena queries the files with SQL, and QuickSight builds the dashboards. Organize the bucket in layers (raw and curated) and partition by date, because Athena charges for the data it scans.
Note to consider: validate the partitions and the Glue schema before building dashboards. A badly defined table returns wrong results without raising an error.
10. Generative AI with RAG on Amazon Bedrock
API Gateway → Lambda → OpenSearch Serverless (vector search) → Bedrock, with documents ingested from S3.
The model does not know your documents. Lambda retrieves the relevant chunks from OpenSearch and sends them to a Bedrock model together with the question. Embeddings must come from the same model during ingestion and at query time.
Note to consider: cost grows with the amount of context you send. Limit the number and size of the retrieved chunks.
11. Managed Kubernetes with EKS
Route 53 → ALB (created by the AWS Load Balancer Controller) → EKS pods → RDS/Aurora, images from ECR.
AWS manages the control plane, and the pods run on worker nodes in private subnets. An Ingress resource creates the ALB for you.
Note to consider: set CPU and memory requests and limits on every pod. Without them, the scheduler cannot spread the load well, and one pod can starve the others.
12. Multi-region and disaster recovery
Route 53 failover + health checks → ECS in us-east-1 (primary) and us-west-2 (secondary) → Aurora Global Database.
Route 53 sends traffic to the primary region while its health checks pass and fails over to the secondary when they do not. Aurora Global Database replicates the data across regions, typically with under one second of lag.
Note to consider: define your RTO and RPO before you choose a strategy. Active-active lowers RTO but roughly doubles compute cost. Pilot light or warm standby cost less but take longer to recover.
From diagram to code: two architectures in Java with the AWS CDK
A diagram is only useful if you can build it. The AWS CDK lets you define the same infrastructure in Java, so the security decisions in the diagram live in typed, reviewable code.
Diagram 1: a private bucket behind CloudFront with OAC
A common mistake is to make the bucket public and point CloudFront at the S3 website endpoint:
// ❌ Common mistake: public bucket, reachable without CloudFront Bucket site = Bucket.Builder.create(this, "SiteBucket") .publicReadAccess(true) .blockPublicAccess(BlockPublicAccess.BLOCK_ACLS) .websiteIndexDocument("index.html") .build(); Anyone can then skip CloudFront, and with it your WAF rules, your HTTPS redirect and your caching. The diagram's version keeps the bucket fully private and lets only CloudFront read it through Origin Access Control:
import software.amazon.awscdk.Stack; import software.amazon.awscdk.StackProps; import software.amazon.awscdk.services.cloudfront.BehaviorOptions; import software.amazon.awscdk.services.cloudfront.Distribution; import software.amazon.awscdk.services.cloudfront.ViewerProtocolPolicy; import software.amazon.awscdk.services.cloudfront.origins.S3BucketOrigin; import software.amazon.awscdk.services.s3.BlockPublicAccess; import software.amazon.awscdk.services.s3.Bucket; import software.amazon.awscdk.services.s3.BucketEncryption; import software.constructs.Construct; public class StaticSiteStack extends Stack { public StaticSiteStack(Construct scope, String id, StackProps props) { super(scope, id, props); // ✅ Private bucket: no public access, TLS only, versioned for rollbacks Bucket site = Bucket.Builder.create(this, "SiteBucket") .blockPublicAccess(BlockPublicAccess.BLOCK_ALL) .encryption(BucketEncryption.S3_MANAGED) .enforceSsl(true) .versioned(true) .build(); // CloudFront reads the bucket through Origin Access Control (OAC). // The CDK adds the bucket policy that allows only this distribution. Distribution.Builder.create(this, "SiteCdn") .defaultBehavior(BehaviorOptions.builder() .origin(S3BucketOrigin.withOriginAccessControl(site)) .viewerProtocolPolicy(ViewerProtocolPolicy.REDIRECT_TO_HTTPS) .build()) .defaultRootObject("index.html") .build(); } } Versioning on the bucket is what makes the "roll back quickly" note in diagram 3 possible.
Diagram 6: an SQS consumer that does not process messages twice
The duplicate-processing bug from diagram 6 usually looks like this:
// ❌ Common mistake: default visibility timeout (30 s) with a 60 s Lambda. // While Lambda is still working, the message becomes visible again // and a second invocation picks it up. Queue orders = new Queue(this, "Orders"); The fix is to make the visibility timeout clearly longer than the function timeout (AWS recommends at least six times) and to add a dead-letter queue for messages that keep failing:
import software.amazon.awscdk.Duration; import software.amazon.awscdk.services.lambda.Code; import software.amazon.awscdk.services.lambda.Function; import software.amazon.awscdk.services.lambda.Runtime; import software.amazon.awscdk.services.lambda.eventsources.SqsEventSource; import software.amazon.awscdk.services.sqs.DeadLetterQueue; import software.amazon.awscdk.services.sqs.Queue; // Messages that fail 3 times are kept here for 14 days for inspection Queue dlq = Queue.Builder.create(this, "OrdersDlq") .retentionPeriod(Duration.days(14)) .build(); Function worker = Function.Builder.create(this, "OrderWorker") .runtime(Runtime.JAVA_21) .handler("com.example.orders.OrderWorker::handleRequest") .code(Code.fromAsset("target/order-worker.jar")) .memorySize(1024) .timeout(Duration.seconds(30)) .build(); // ✅ Visibility timeout = 6 x the Lambda timeout Queue orders = Queue.Builder.create(this, "Orders") .visibilityTimeout(Duration.seconds(180)) .deadLetterQueue(DeadLetterQueue.builder() .queue(dlq) .maxReceiveCount(3) .build()) .build(); // Report partial batch failures so one bad message // does not force the whole batch to be retried worker.addEventSource(SqsEventSource.Builder.create(orders) .batchSize(10) .reportBatchItemFailures(true) .build()); With reportBatchItemFailures turned on, the handler returns only the IDs of the messages that failed:
public class OrderWorker implements RequestHandler<SQSEvent, SQSBatchResponse> { @Override public SQSBatchResponse handleRequest(SQSEvent event, Context context) { List<SQSBatchResponse.BatchItemFailure> failures = new ArrayList<>(); for (SQSEvent.SQSMessage message : event.getRecords()) { try { process(message.getBody()); // must be idempotent: SQS is at-least-once } catch (Exception e) { failures.add(new SQSBatchResponse.BatchItemFailure(message.getMessageId())); } } return new SQSBatchResponse(failures); } } Even with the right timeout, SQS standard queues deliver at least once, so process should be idempotent. For example, use the order ID as a conditional write key in DynamoDB.
Best practices across the 12 diagrams
A few rules repeat across the catalog:
- Expose one entry point. CloudFront, the ALB or API Gateway faces the internet. Buckets, containers and databases stay private.
- Separate intake from processing. Queues (diagram 6) and event buses (diagram 7) let you absorb spikes and retry failures without losing requests.
- Treat configuration as part of the architecture. OAC, security groups, visibility timeouts and pod limits are not details to fix later. They decide whether the design works.
- Know what you pay for. WAF rules, Athena scans, AppSync connections and Bedrock context all scale with usage. Each diagram's note points out the main cost driver.
- Decide RTO and RPO before you design for resilience. Multi-AZ, multi-region and active-active are answers to different questions.
Conclusion
The catalog is meant as a map: start with the static site, add security and CI/CD, move to containers or serverless, then add asynchronous processing, events, real time, analytics, generative AI, Kubernetes and multi-region resilience as the problem requires.
Key takeaways:
- Learn AWS services in combinations, not one by one.
- Each architecture has one detail that usually breaks it. Learn that detail early.
- Infrastructure as code in Java with the CDK turns those details into code you can review.
Browse the diagrams at https://josesaid.github.io/aws-architecture-diagrams/. The source is on GitHub. Which architecture would you add as number 13?

