AWS SAA-C03: How to Design Highly Available and Fault-Tolerant Architectures

AWS SAA-C03: How to Design Highly Available and Fault-Tolerant Architectures

What high availability and fault tolerance really mean in AWS

For SAA-C03, the real skill isn’t just spotting service names. It is matching the requirement to the failure domain. High availability means the workload can keep running, maybe with just a brief hiccup, when a component fails or an Availability Zone goes sideways. Fault tolerance is a higher bar altogether. The system keeps going with little to no interruption for the failures it was designed for, usually by running active-active or fully redundant components in parallel. Resiliency is the bigger picture: take the hit, degrade in a controlled way, and bounce back fast. Disaster recovery is your cross-Region fallback plan for when staying in one Region just isn’t enough.

That distinction matters because many AWS answers are highly available, but not truly fault-tolerant. An EC2 Auto Scaling Group across multiple AZs behind an ALB is highly available. A multi-Region active-active design with health-based traffic steering and replicated data is closer to fault tolerance for certain failures. On the exam, backup and restore is recovery, not failover.

Here’s the mental model I keep coming back to: if an instance fails, I’m thinking health checks and replacement. If an AZ fails, I’m thinking multi-AZ design. If a Region fails, that’s multi-Region DR territory. If traffic spikes, it’s elasticity and buffering. If a dependency fails, I’m looking at decoupling and graceful degradation. And if a network path breaks, redundant routing or connectivity is the name of the game.

Failure domains, service scope, and design rules

The exam rewards candidates who understand scope. EBS is zonal. A NAT Gateway lives in one AZ, so in practice I usually want each private subnet to route to a NAT Gateway in the same AZ. That way, I’m not sneaking in a hidden single point of failure. ALB, API Gateway, Lambda, DynamoDB, and S3 are regional services, but don’t let that fool you for a second—regional doesn’t mean multi-Region. Route 53 and CloudFront sit in that global-service bucket, which is exactly why they show up so often in resilience designs. Transit Gateway is regional unless you deliberately connect Regions.

The core rules don’t really change: remove single points of failure, spread across AZs, automate recovery, keep state outside the app where it makes sense, and lean on managed services when they lower operational risk. And just as importantly, durability isn’t the same thing as availability. S3 buckets live in a specific Region, and S3 stores your objects redundantly across multiple AZs in that Region by design. That gives you fantastic durability, sure, but your application still needs a resilient way to get to that data without creating a new bottleneck.

Requirement Typical pattern Common trap
Survive instance failure Load balancer + Auto Scaling / managed compute One large EC2 instance
Survive AZ failure Multi-AZ subnets, compute, and data tier All resources in one AZ
Survive Region failure Multi-Region DR or active-active Assuming Multi-AZ is enough
Buffer spikes SQS, async processing SNS alone
Fan-out events SNS or EventBridge SQS as pub/sub
Shared POSIX file storage EFS EBS across many instances
Automatic DB failover RDS Multi-AZ / Aurora Read Replica

Compute and load balancing

A classic highly available web design is EC2 instances in at least two AZs behind an ALB, with an Auto Scaling Group spread across subnets in both AZs. An ALB only becomes resilient when you attach it to subnets in multiple AZs; just creating the load balancer by itself isn’t enough. ALB supports Layer 7 routing, TLS termination, host/path routing, and target groups. NLB is Layer 4, useful for TCP/UDP, very high throughput, and static IP needs. Gateway Load Balancer is for inserting inspection appliances.

For EC2, health signals matter. EC2 status checks, ALB target group health checks, and Auto Scaling replacement behavior are different things. Tune target group health checks with the right path, matcher, interval, and thresholds. Deregistration delay helps in-flight requests drain cleanly. Default instance warm-up and target-tracking policies help scaling react without flapping.

Stateless design is a major availability advantage. Keep sessions in ElastiCache for Redis, DynamoDB, or another shared store rather than local disk. Otherwise instance replacement breaks user continuity. Cross-zone load balancing is on by default with ALB, so traffic gets spread around nicely, but that still doesn’t replace the need for healthy targets in more than one AZ.

Lambda and API Gateway take a lot of host-management risk off your plate because AWS handles the underlying fleet placement across AZs within the Region. But serverless definitely isn’t magic. Concurrency quotas, throttling, timeouts, VPC ENI behavior, and downstream bottlenecks can still bite you if you’re not paying attention. For HA-sensitive designs, reserved concurrency can protect your critical functions, provisioned concurrency can help reduce cold-start pain, and SQS can soak up bursts so Lambda doesn’t end up hammering the database. If Lambda’s talking to a relational database, RDS Proxy can really help smooth out connection storms during scaling or failover.

ECS and EKS can be highly available when tasks or pods are spread across AZs. With ECS, service desired count and capacity provider strategy matter. With EKS, the control plane is managed and regional, but your worker nodes still have to be spread across AZs if you want real resilience. I’d use topology spread constraints, multiple node groups, and Pod Disruption Budgets to keep placement resilient and avoid clustering everything in one failure domain.

Storage and file service choices

S3 is regional object storage, and by default it stores data redundantly across multiple AZs. Versioning absolutely helps with accidental deletion or overwrites, but by itself it won’t save you from every kind of logical corruption or malicious deletion. For stronger recovery posture, combine Versioning with Object Lock, careful replication design, and backup isolation. Cross-Region Replication requires versioning and the right IAM and KMS permissions. It is asynchronous, so low RPO is possible, not zero. Replication can also copy bad writes or delete markers if configured that way, so it is not a substitute for immutable recovery controls.

EBS is zonal block storage. Good for boot volumes and single-instance block access, but not shared multi-AZ storage. Snapshots are incremental and stored in S3, making them useful for restore into another AZ or Region, but restore is recovery, not live failover.

EFS is the shared file answer when multiple Linux instances need the same files. Be precise: Regional EFS supports multi-AZ availability through mount targets in multiple AZs. EFS One Zone is not the HA choice. EFS Replication can support cross-Region or cross-account DR patterns.

FSx is specialized, so match the flavor to the requirement. FSx for Windows File Server supports Windows-native shares and can be deployed Multi-AZ. FSx for NetApp ONTAP includes HA pair concepts and enterprise file features. FSx for Lustre is for high-performance compute, not a generic shared-app-storage answer. FSx for OpenZFS fits specific NFS and performance needs.

Databases, caching, and failover

RDS Multi-AZ is the classic answer for relational HA within one Region. It provides automatic failover across AZs, not read scaling. Failover is not instantaneous, and applications must reconnect using the DB endpoint. Also keep in mind the difference between traditional Multi-AZ DB instance deployments and the newer Multi-AZ DB cluster options for supported engines. Either way, Multi-AZ improves availability, but it doesn’t replace backups when you’re dealing with corruption, operator mistakes, or ransomware.

Read Replicas are for read scaling and asynchronous copies. They can be promoted manually or during certain workflows, but they are not the default answer for automatic HA failover. Aurora uses distributed storage across multiple AZs and can offer faster failover and stronger cross-Region options such as Aurora Global Database, but it is not automatically “better” in every case; engine support, cost, and compatibility still decide.

DynamoDB is already highly available within a Region, so you’re starting from a pretty strong place. On-Demand mode is especially handy when traffic is unpredictable, because it lets the service absorb that variation without you having to guess capacity up front. Global Tables give you multi-Region active-active replication, but you’ve got to describe them carefully. They replicate across Regions with multi-active writes, and conflict handling is based on last-writer-wins. That is not the same as strong cross-Region consistency.

Caching improves availability indirectly by reducing load on fragile backends. ElastiCache for Redis with Multi-AZ replication groups can help absorb read pressure and smooth failovers. DAX improves DynamoDB read latency and is deployed as a cluster, but it is a cache layer, not your durability or DR strategy.

Service Best use Not for
RDS Multi-AZ Automatic relational failover in one Region Read scaling
Read Replica Read scaling, async copy Primary HA by itself
Aurora Global Database Low-RTO cross-Region relational DR Cheap/simple DR
DynamoDB Global Tables are the multi-Region answer when you want active-active behavior for a NoSQL workload. Multi-Region active-active NoSQL Relational workloads

Networking, DNS, and edge resilience

A resilient VPC usually has public and private subnets spread across at least two AZs. If private workloads need internet egress, I’d put a NAT Gateway in each AZ and point each private subnet route table to the local NAT Gateway. The real hidden single point of failure usually isn’t NAT Gateway itself—it’s the design that puts all egress through one AZ.

VPC endpoints reduce both cost and failure exposure. Gateway endpoints are common for S3 and DynamoDB and update route tables directly. Interface endpoints place elastic network interfaces in subnets and use security groups. If private workloads mostly talk to AWS services, VPC endpoints can remove a lot of the NAT dependency altogether, which is honestly a nice reliability win.

Route 53 gives you DNS-based failover, latency-based routing, weighted cutovers, and multivalue answers, so you’ve got a lot of routing flexibility to work with. Alias records to ALB, CloudFront, and other AWS targets show up a lot in exam scenarios, and sometimes you can use Evaluate Target Health instead of building separate health checks. DNS failover is never truly instant, because TTL values and client-side caching still matter more than people expect.

CloudFront improves edge performance and resilience through caching, origin shielding, and WAF integration, which is why it’s so useful in real-world designs. It does not replace origin HA, but origin groups can provide origin failover for supported patterns. That is useful for static or semi-dynamic architectures, though full multi-Region application resilience still requires replicated backends.

For hybrid resilience, one Direct Connect is not enough. A stronger hybrid design uses multiple Direct Connect connections—ideally in separate locations or even with separate providers—plus Site-to-Site VPN backup and BGP-based failover. Transit Gateway makes multi-VPC and hybrid routing much easier, but it’s still regional, so if you need cross-Region resilience you’ll need separate regional design or inter-Region connectivity.

Decoupling, queues, and workflow reliability

SQS is there to buffer work. Standard queues give you at-least-once delivery and best-effort ordering, which is great for buffering but not for pretending duplicates can’t happen. FIFO queues provide ordering and deduplication, but they come with different throughput characteristics, so there’s always a tradeoff. Visibility timeout, message retention, long polling, and DLQ redrive settings are practical tuning points. Also remember a source queue and its DLQ should be the same type: standard-to-standard or FIFO-to-FIFO.

SNS is pub/sub fan-out, not workload buffering. EventBridge is event routing with rules, integration patterns, and archive/replay capabilities. Step Functions adds orchestration, retries, branching, and catch logic. It definitely improves workflow reliability and visibility, but true end-to-end exactly-once behavior still depends on application idempotency.

Honestly, idempotency is one of the most practical resilience topics on this exam. Retries happen. Use idempotency keys, conditional writes in DynamoDB, deduplication where it makes sense, and safe retry logic so a duplicate message doesn’t accidentally turn into a duplicate business action.

DR strategy and backup integrity

Choose DR by RTO and RPO. Backup and restore is cheapest and slowest. Pilot light keeps core services and replicated data ready. Warm standby runs a scaled-down environment in another Region. Active-active is the fastest and most expensive. Multi-AZ is an HA answer inside one Region; it is not a Region-level DR answer.

Also separate replication from recovery integrity. Replication can also faithfully copy corruption, bad writes, or accidental deletes, which is exactly why people get burned when they assume replication equals protection. For a stronger recovery posture, I’d use AWS Backup with cross-account or cross-Region copies, protected vaults, and retention policies. For S3, Versioning plus Object Lock gives you much stronger protection than Versioning alone, especially when you’re worried about deletion or tampering. Test failover and failback regularly, because untested DR is basically just a guess dressed up as a plan.

Deployable-style partial IaC example

Here’s a partial CloudFormation example—not a full template—but it uses parameters and dynamic AZ selection instead of hardcoding AZ names, which is the cleaner way to do it.

Parameters: VpcId: Type: AWS::EC2::VPC::Id — this tells CloudFormation to expect an existing VPC ID as the input. Resources: PublicSubnetA: Type: AWS::EC2::Subnet — here we’re defining the second subnet. — this defines the subnet resource. Properties: VpcId: !Ref VpcId — again, this attaches the subnet to the same VPC. — this points the subnet at the VPC parameter you passed in. AvailabilityZone: !Select [0, !GetAZs ''] — this places the subnet in the first available AZ for the Region. CidrBlock: 10.0.0.0/24 — this gives the subnet its address range. PublicSubnetB: Type: AWS::EC2::Subnet — here we’re defining the second subnet. — this defines the subnet resource. Properties: VpcId: !Ref VpcId — again, this attaches the subnet to the same VPC. — this points the subnet at the VPC parameter you passed in. AvailabilityZone: !Select [1, !GetAZs ''] — this puts the second subnet in another AZ, which is what you want for HA. CidrBlock: 10.0.1.0/24 — this is the second subnet range. AppDLQ: Type: AWS::SQS::Queue — same resource type again, but this one is the dead-letter queue. — this creates the main queue or the DLQ, depending on which resource you’re defining. AppQueue: Type: AWS::SQS::Queue — same resource type again, but this one is the dead-letter queue. — this creates the main queue or the DLQ, depending on which resource you’re defining. Properties: RedrivePolicy: deadLetterTargetArn: !GetAtt AppDLQ.Arn — this sends messages that keep failing over to the DLQ instead of retrying forever. maxReceiveCount: 5

Troubleshooting patterns the exam loves to test

IIf you’ve got an ALB but traffic still isn’t getting through, check target group health, security groups, the health check path, and whether the targets are actually spread across AZs. If Auto Scaling isn’t replacing instances, I’d look at the health check type, suspended processes, launch template errors, and instance warm-up settings. If RDS has already failed over but the app is still throwing errors, I’d suspect connection caching, stale DNS, or hardcoded IPs. If private subnets lose internet egress, check the route tables first; a lot of NAT “failures” are really just bad route associations. If the SQS backlog keeps growing, check consumer concurrency, visibility timeout, Lambda throttling, and DLQ growth, because one of those is usually the real bottleneck. If Route 53 failover feels slow, remember that TTL and client-side caching are often the real reason it’s not switching as fast as you expected.

Exam traps and rapid review

Use these elimination rules:

  • Automatic failover required? Backup-only answers are wrong.
  • AZ failure required? Single-AZ NAT, EC2, or database answers are wrong.
  • Read scaling required? Multi-AZ alone is not enough.
  • HA required? Read Replica alone is not enough.
  • Shared file storage required? EBS is usually wrong.
  • Buffering required? SNS alone is usually wrong.
  • Fan-out required? SQS alone is usually wrong.
  • Region failure required? Multi-AZ alone is incomplete.

Final cheat sheet: RDS Multi-AZ = HA/failover; Read Replica = read scaling. S3 = regional bucket with multi-AZ durability. EFS Regional = shared multi-AZ file storage; EFS One Zone = not the HA pick. NAT Gateway = zonal, so route locally per AZ. Lambda/API Gateway, ALB, DynamoDB, and S3 are regional services, but they’re not automatic multi-Region solutions. Route 53 failover works through DNS, so TTL matters a lot. CloudFront helps resilience, but origin design still matters.

If you start with the failure domain, then think about service scope and tradeoffs, you’ll get these questions right far more often than if you just memorize service names in isolation.