AWS SAA-C03: Databases & Caching — Study Guide
Part of the AWS SAA-C03 Complete Study Guide. Practice these concepts with verified answers in the AWS/Amazon exam hub, or take timed practice tests on ExamRoll.io.
Amazon RDS: Managed Relational Engines
Amazon RDS provides six managed engines — MySQL, PostgreSQL, MariaDB, Oracle, SQL Server, and Amazon Aurora (MySQL- and PostgreSQL-compatible) — abstracting patching, backups, replication topology, and failover. Engine choice drives licensing, backup semantics, and feature availability: Oracle and SQL Server carry BYOL vs. License Included distinctions, SQL Server supports up to five read replicas via native replication, and Oracle read replicas require Enterprise Edition with Active Data Guard.
The two RDS features most heavily leveraged for availability and scale — and the two most frequently confused — are Multi-AZ deployments and read replicas. They are orthogonal, complementary, and not interchangeable.
Multi-AZ provisions a synchronous standby replica in a second Availability Zone. Every write is committed to both the primary and the standby before acknowledgment, giving an RPO effectively at zero and an RTO of 60–120 seconds during automatic failover. The standby accepts no traffic — it exists solely for failover. When the primary fails, an AZ is impaired, or maintenance requires a restart, RDS flips the DNS CNAME to the standby and applications reconnect transparently through the same endpoint. Multi-AZ is the least-effort fix for a single-AZ single point of failure: it is a configuration toggle, requires no schema or application changes, and preserves the endpoint. The newer Multi-AZ cluster deployment is a three-node topology (one writer, two readable standbys) using semi-synchronous replication with roughly one-second commit latency, giving HA plus limited read offload.
Read replicas use the engine’s native asynchronous replication (MySQL binlog, PostgreSQL WAL streaming). They are the correct tool for read scaling — analytical queries, reporting dashboards, ad-hoc SELECTs — that would otherwise starve OLTP on the primary. The canonical scenario: an order-processing database timing out because employees run long month-end reports. Adding a read replica and pointing the reporting tool at its endpoint isolates analytical load without upsizing the primary. Read replicas can be cross-region and can be promoted manually to standalone databases, but promotion is never automatic and any transactions not yet replicated at the moment of failure are lost.
Two traps recur in this space. First, treating read replicas as an HA solution is wrong on multiple grounds: replicas are asynchronous (potential data loss), have no automatic promotion, the endpoint changes on promotion, and in-flight transactions vanish. If a design promises “zero data loss” and points to a read replica as the failover target, that promise is false. Second, provisioning Multi-AZ to serve read traffic wastes money because the standby is not readable in the standard topology. Multi-AZ solves availability; read replicas solve read scale; you often want both.
Read replicas also do nothing for write scaling — every write still hits the primary and replicas must replay it. For write scale, you shard, move to Aurora (decoupled storage), or redesign toward DynamoDB.
Right-Sizing Read Replicas
A replica does not have to match the primary’s instance class. The primary handles full write load plus reads; a replica handles only the reads that hit it. A db.r6i.4xlarge primary paired with a db.r6i.large replica for a nightly reporting job is entirely reasonable — provided the replica keeps up with replication I/O. The methodology is: measure the replica’s actual CPU, memory, and replica lag, then size to that workload. The one caveat: if you intend to promote a replica to become the primary during DR, it must be sized to handle write load. Undersized replicas are not promotion targets.
Blue/Green Deployments and Storage
RDS Blue/Green Deployments create a full staging copy of production (green) that stays in sync via logical replication. You upgrade the engine version, change parameter groups, or alter schemas on green, test it, and switch over in under a minute with automatic endpoint renaming. This eliminates the classic in-place upgrade risk where a failed major-version upgrade forces a snapshot rollback.
Storage selection matters as much as instance class for write-heavy OLTP. The gp3/io2 taxonomy:
| Type | Baseline | Max IOPS | Use case |
|---|---|---|---|
| gp3 | 3,000 IOPS / 125 MB/s (independent of size) | 16,000 | General purpose; IOPS/throughput decoupled from capacity |
| io2 Block Express | Provisioned | 256,000 | Mission-critical OLTP, SAP, large Oracle |
| io2 Multi-Attach | Provisioned | 256,000 | Shared-disk clusters (Oracle RAC-like) |
The gp3 improvement over gp2 is decoupling IOPS from size — you no longer over-provision capacity for performance. Choose io2 when sustained IOPS exceed gp3’s ceiling or when 99.999% durability is required. Multi-Attach lets a single io2 volume attach to up to 16 Nitro instances, but the filesystem or application must be cluster-aware; it is not a substitute for replication. A latent outage trap: provisioning fixed storage without storage autoscaling enabled or a CloudWatch alarm on FreeStorageSpace. When free space hits zero, RDS enters storage-full and rejects writes.
Amazon Aurora: Architecture and Endpoints
Aurora reimplements the storage layer beneath MySQL and PostgreSQL as a distributed, log-structured, six-way replicated volume spanning three AZs. Compute nodes are stateless relative to storage, so an Aurora Replica reads from the same underlying volume as the writer rather than replaying a log. Replication lag is typically 10–20 ms, versus seconds for standard RDS replicas, and a cluster supports up to 15 replicas that can be promoted to writer in about 30 seconds.
Aurora exposes four endpoint types:
| Endpoint | Purpose |
|---|---|
| Cluster (writer) endpoint | Always points to the current primary |
| Reader endpoint | Load-balances connections across all replicas |
| Custom endpoint | Routes to a specific subset of instances you choose |
| Instance endpoint | Direct access to a single node |
Custom endpoints matter when replicas are heterogeneous. If three of six replicas are db.r6g.8xlarge for analytical reporting while the rest serve OLTP reads, the general reader endpoint occasionally routes reporting to smaller nodes, hurting predictability. A custom endpoint scoped to only the large replicas gives deterministic workload isolation:
aws rds create-db-cluster-endpoint \
--db-cluster-identifier prod-aurora \
--db-cluster-endpoint-identifier reporting \
--endpoint-type READER \
--static-members reporting-node-1 reporting-node-2 reporting-node-3
Note that Aurora endpoint DNS TTLs are 5 seconds; caching connection strings longer defeats failover behavior.
Aurora Auto Scaling adds and removes readers based on target CPU or connection metrics, which is the canonical answer for unpredictable, read-heavy workloads that must remain highly available:
TargetTrackingScalingPolicyConfiguration:
PredefinedMetricSpecification:
PredefinedMetricType: RDSReaderAverageCPUUtilization
TargetValue: 60.0
ScaleInCooldown: 300
ScaleOutCooldown: 60
When RDS MySQL read replicas cannot keep replication lag under one second at peak, the low-code answer is to migrate to Aurora MySQL — the connection strings barely change, and storage-level replication eliminates the lag.
Aurora Serverless v2 and Cloning
Aurora Serverless v2 scales compute vertically in fine-grained Aurora Capacity Units (ACUs, 2 GiB memory each) in about half a second, without disconnecting sessions. It suits variable workloads with a known baseline memory footprint — for example, an on-prem MySQL migration that always consumes at least 2 GiB. Set min at 1 ACU, max at 32 ACUs, and the cluster flexes without administration. Provisioned Aurora remains preferable when load is steady and predictable, since Serverless v2 carries a per-ACU premium.
Aurora cloning creates a new cluster sharing the source’s storage pages via copy-on-write. Clones appear in seconds and cost nothing until data diverges, making them ideal for staging environments, risky migrations, or letting analysts hammer a production copy. A snapshot restore physically rehydrates data and can take hours; cloning wins when speed matters.
Aurora Global Database
Aurora Global Database extends a cluster to up to five secondary Regions using dedicated storage-layer replication infrastructure — not binlog shipping. Typical replication lag is under one second, RPO is under one second, and managed failover promotes a secondary in under a minute.
| Feature | Cross-Region Read Replica | Aurora Global Database |
|---|---|---|
| Typical RPO | ~1 minute | < 1 second |
| Typical RTO | 15–60 minutes | < 1 minute (managed failover) |
| Replication path | Binary log over network | Dedicated storage-layer infrastructure |
| Managed failover | No | Yes |
Two critical semantics: secondary regions are read-only during normal operation, and Global Database is designed for low-RPO DR and low-latency remote reads, not active-active multi-master. Assuming a global DB “automatically handles writes in the secondary region” is wrong — writes there require an explicit managed failover or detach-and-promote. For a stated requirement of 5-minute RPO / 20-minute RTO across regions with minimal operational overhead, Global Database is the canonical answer.
RDS Proxy and Connection Management
Serverless and highly concurrent workloads compound a classic problem: connection storms. A Lambda function scaling to 3,000 concurrent executions opens 3,000 sockets, blowing through max_connections and causing cascading failures — precisely when the system is under load. In-function pools do not help because each concurrent execution environment is isolated.
RDS Proxy sits between clients and the database, maintaining a warm pool of connections and multiplexing client sessions onto them. It solves two problems:
- Connection storms. Thousands of client sessions are multiplexed onto a small backend pool.
- Failover time. The proxy holds client connections open while re-establishing the backend link, cutting perceived failover time by up to 66% and eliminating client-side TCP/TLS re-establishment and DNS re-resolution.
It integrates with IAM and Secrets Manager for credential handling, removing hard-coded secrets from application code.
DBProxy:
Type: AWS::RDS::DBProxy
Properties:
EngineFamily: POSTGRESQL
RequireTLS: true
IdleClientTimeout: 1800
Auth:
- AuthScheme: SECRETS
SecretArn: !Ref DBSecret
IAMAuth: REQUIRED
Applications connect to the proxy endpoint, not the cluster endpoint. Any Lambda-to-RDS or high-fan-out architecture should use RDS Proxy unless there is a specific reason not to. Running your own pooler (PgBouncer, ProxySQL) on EC2 is possible but adds the operational overhead the proxy exists to eliminate.
DynamoDB: Capacity Modes
DynamoDB is a managed key-value/document store with single-digit-millisecond latency at any scale, horizontally partitioned by hash key. It offers two capacity modes:
| Mode | Best for | Billing | Behavior under spikes |
|---|---|---|---|
| Provisioned | Predictable, steady traffic | RCU/WCU per hour | Throttles unless auto-scaling configured |
| On-Demand | Unknown, spiky, or new workloads | Per-request | Absorbs traffic instantly up to table limits |
On-demand is dramatically simpler but costs roughly 6–7× more per request than well-utilized provisioned capacity. For a nightly 4-hour batch at 500 WCU, on-demand overpays significantly — provisioned with scheduled scaling or reserved capacity is far cheaper. Conversely, using provisioned for an unpredictable public-launch workload leads to throttling. For steady workloads with moderate variance, provisioned with target-tracking auto-scaling around 70% utilization is meaningfully cheaper than on-demand — often by 50–70%:
TargetTrackingScalingPolicyConfiguration:
TargetValue: 70.0
PredefinedMetricSpecification:
PredefinedMetricType: DynamoDBReadCapacityUtilization
ScaleInCooldown: 60
ScaleOutCooldown: 60
You can switch modes once every 24 hours. Assuming on-demand is universally cheaper is a costly mistake; so is assuming provisioned is always right for spiky traffic.
DynamoDB: Consistency, Streams, Global Tables
Reads default to eventually consistent (may return stale data within ~1 second, costs 0.5 RCU). Setting ConsistentRead=true returns the latest committed value at double the cost. Strongly consistent reads are not supported through global secondary indexes or through DAX — those paths always return eventually consistent data.
DynamoDB Streams capture item-level changes as an ordered log retained for 24 hours, triggering Lambda for downstream processing (search indexing, notifications, cross-table denormalization) without polling.
Global Tables build on Streams to provide multi-active, multi-Region replication with last-writer-wins conflict resolution. They are the right answer when reads and writes must be local in multiple Regions. But they roughly double storage and write costs — every write is a WCU in every replica Region — and weaken consistency across Regions. The trap is enabling Global Tables when a single Region satisfies availability requirements: DynamoDB in a single Region is already replicated across three AZs at 99.99% availability. Cost-effective single-Region HA is a single-Region table with PITR enabled and, if desired, provisioned capacity with auto-scaling.
Point-in-Time Recovery (PITR) provides continuous backups with per-second restore granularity for the last 35 days at negligible overhead. Enable it on any production table — it satisfies typical RPO requirements of minutes rather than hours. For longer retention (regulatory holds), integrate AWS Backup for scheduled, lifecycle-managed, cross-region-copyable backups.
TTL lets you specify an attribute containing a Unix epoch expiration; DynamoDB asynchronously deletes expired items at no cost, ideal for session stores, ephemeral tokens, or event caches. TTL deletions flow through Streams for downstream archival:
TTLSpecification:
AttributeName: expireAt
Enabled: true
For analytics, export to S3 produces a point-in-time snapshot readable by Athena, Redshift Spectrum, or EMR without consuming table capacity — a much better pattern than scanning the table.
DAX: DynamoDB Accelerator
DAX is a fully managed, in-memory, write-through cache specifically for DynamoDB, delivering microsecond read latency versus DynamoDB’s single-digit-millisecond baseline. Its distinguishing feature is API compatibility: the DAX client is a drop-in replacement for the DynamoDB SDK client, so applications adopt it by changing endpoint configuration rather than rewriting query logic:
import amazondax
dax = amazondax.AmazonDaxClient(
endpoint_url='dax://cluster.abc.dax-clusters.us-east-1.amazonaws.com')
table = dax.Table('Products')
resp = table.get_item(Key={'sku': '1234'}) # microsecond hit path
DAX maintains two caches: an item cache for GetItem/BatchGetItem results and a query cache for Query/Scan results. Writes are write-through — DAX proxies to DynamoDB and updates its item cache on success — but the query cache relies on TTL, so query results can grow stale even for freshly written items.
Two constraints matter. First, DAX only accelerates eventually consistent reads; strongly consistent reads bypass the cache. Second, other writers bypassing DAX cause staleness. For a product-detail page hit millions of times daily, DAX is the least-operational-overhead accelerator — no cache-invalidation code, no separate cluster management. ElastiCache in front of DynamoDB would work but requires cache-aside logic that DAX makes unnecessary.
ElastiCache: Redis and Memcached
ElastiCache delivers sub-millisecond, in-memory data access as a managed service running Redis or Memcached. Engine choice is feature-driven:
- Memcached — pure multi-threaded key/value cache with horizontal sharding, no persistence, no replication, no pub/sub. Use only for ephemeral caching where losing the entire cache is acceptable.
- Redis — supports replication, Multi-AZ with automatic failover, cluster mode for sharding, persistence, pub/sub, sorted sets, transactions, and encryption in transit and at rest. Required for anything durable or requiring complex data types.
Two canonical patterns dominate.
Centralized session store. When an ALB fans traffic across stateless EC2 or ECS instances, local session storage forces sticky sessions, which unbalances load and breaks during scale-in, deployments, and AZ failures. Externalizing sessions to Redis lets any instance serve any request, and sessions survive host failure:
import redis, json
r = redis.Redis(host='sessions.abc123.ng.0001.use1.cache.amazonaws.com',
port=6379, ssl=True)
def save_session(sid, data, ttl=1800):
r.setex(f"sess:{sid}", ttl, json.dumps(data))
Read offload for expensive queries — leaderboards (Redis sorted sets via ZADD/ZREVRANGE), catalog lookups, aggregations. Caching strategies must match consistency needs:
- Lazy loading (cache-aside): app reads cache; on miss, reads DB and populates cache with a TTL. Simple, but cold misses hurt and data can be stale.
- Write-through: app writes to cache and DB in the same operation. Cache stays fresh, but writes are slower and unused data still occupies memory.
- Write-back (write-behind): writes go to cache first, asynchronously flushed to DB. Fastest writes, but a cache failure loses data.
data = r.get(f"product:{sku}")
if data is None:
data = db.query("SELECT * FROM products WHERE sku=%s", sku)
r.setex(f"product:{sku}", 300, serialize(data))
Relying on any caching layer without an invalidation strategy — TTL, explicit DEL on update, or write-through — produces stale reads. The failure mode is silent: the application appears correct until users notice divergence. Caching is also often the cheapest way to scale reads past what read replicas comfortably support and shields the primary during traffic surges.
Unlike DAX, ElastiCache is engine-agnostic — you own the invalidation logic — which is why DAX wins on operational simplicity when the backing store is DynamoDB.
Migration: DMS and SCT
AWS Database Migration Service (DMS) replicates data between homogeneous engines (Oracle→Oracle, MySQL→Aurora MySQL) or heterogeneous ones (Oracle→Aurora PostgreSQL, SQL Server→RDS MySQL, on-prem→DynamoDB). A DMS task has three phases:
- Full load — bulk-copy existing rows.
- CDC (change data capture) — tail the source’s transaction log and apply ongoing changes.
- Full load + CDC — the common minimal-downtime pattern: source stays online, DMS keeps target in sync, cutover is a short DNS switch.
aws dms create-replication-task \
--replication-task-identifier ora-to-aurora \
--source-endpoint-arn $SRC --target-endpoint-arn $TGT \
--migration-type full-load-and-cdc \
--table-mappings file://mappings.json \
--replication-instance-arn $RI
DMS Serverless removes the need to size and manage replication instances — capacity auto-provisions to workload, suiting variable or long-running CDC. Source engines must have supplemental logging (Oracle) or ROW-format binary logging (MySQL) enabled. For very large seeds, DMS integrates with Snowball Edge for offline loads.
DMS moves data, not schema. For heterogeneous migrations you pair it with the AWS Schema Conversion Tool (SCT), which converts stored procedures, views, triggers, sequences, and dialect-specific types — for example, Oracle PL/SQL to PostgreSQL PL/pgSQL, or T-SQL to Aurora MySQL. SCT produces an assessment report flagging objects requiring manual rework (typically 5–20% for complex codebases). Running DMS alone for a cross-engine migration is a common mistake: while DMS can create rudimentary target tables, it does not translate procedures or proprietary types correctly. For homogeneous migrations, SCT is unnecessary — native tools (mysqldump, pg_dump, RMAN) plus DMS CDC suffice.
The full cross-engine pattern:
1. SCT: convert schema, apply to target RDS/Aurora
2. DMS full-load task: bulk copy existing data
3. DMS CDC task: capture ongoing changes from source
4. Cutover: stop writes at source, wait for CDC lag = 0, redirect app
Backups and Point-in-Time Recovery
RDS automated backups combine daily snapshots with 5-minute transaction log backups, enabling PITR to any second within the retention window (1–35 days, default 7). Restoration creates a new instance — you cannot restore in place — so application endpoints or CNAMEs must be updated. Manual snapshots persist beyond retention and survive instance deletion (subject to the final-snapshot setting), can be copied cross-Region for DR, and can be shared across accounts.
Fast Snapshot Restore (FSR) for EBS-backed snapshots eliminates the lazy-load penalty, giving restored volumes full performance immediately — useful when spinning up multiple environments from one snapshot under time pressure. Aurora cloning bypasses snapshots entirely for same-region copies. DynamoDB PITR is a parallel feature that must be enabled per table and produces a new table on restore.
Purpose-Built Databases
Selecting a purpose-built engine when access patterns demand it prevents expensive re-architecture. Amazon Neptune is a managed graph database supporting Gremlin, openCypher, and SPARQL — appropriate when queries traverse relationships (fraud rings, social graphs, knowledge graphs) where recursive joins in a relational engine would be prohibitively expensive. Amazon QLDB is an immutable, cryptographically verifiable ledger with an append-only journal, suited to systems of record requiring tamper-evident audit — supply chain provenance, financial transactions, DMV records. DynamoDB is the default for single-digit-millisecond key-value or document access at any scale, using patterns such as single-table design, composite sort keys for hierarchical access, and GSIs for alternate access paths. Forcing these workloads into RDS creates lock contention (ledger writes), query complexity (graph traversal), or scaling ceilings (high-throughput key-value) — each far costlier to remediate later than choosing correctly at design time.
Trap Summary
- Read replicas as HA. Asynchronous, no auto-promotion, endpoint changes on promotion, in-flight transactions lost. Multi-AZ is the HA answer.
- Multi-AZ for read scaling. Standby is not readable in standard RDS Multi-AZ. Use read replicas or Aurora replicas.
- Single-AZ production. No failover target; loses availability on any AZ event or instance restart. Multi-AZ is roughly 2× cost for a qualitative availability jump.
- Replica sized equal to primary by default. Replicas often need much less; size to measured workload unless the replica is a promotion target.
- On-demand DynamoDB always cheaper. It is roughly 6–7× per-request cost; provisioned + auto-scaling wins for steady workloads.
- Global Tables for single-region needs. Doubles cost and weakens consistency without benefit unless multi-region user proximity or DR is required.
- Aurora Global Database secondaries serve writes. They are read-only until managed failover promotes them.
- Lambda → RDS without RDS Proxy. Connection storm exhausts
max_connections; in-function pools do not help across concurrent execution environments. - DMS alone for heterogeneous migration. Moves data, not schema. Pair with SCT.
- Fixed RDS storage without autoscaling or
FreeStorageSpacealarms. Latent outage —storage-fullstate rejects writes. - Caching without invalidation strategy. Silent staleness. TTLs, write-through, or explicit invalidation are non-negotiable.
- Strongly consistent reads through DAX or GSIs. Both paths only serve eventually consistent data.
← Previous: Content Delivery · All domains · Next: Analytics →
Practice Databases questions → · Timed practice on ExamRoll.io →
Pass the whole exam — not just this question
You found this answer. Get every verified question and explanation in one place, and save hours of prep. Free to start.
Pass your exam →