RDS, Aurora & DynamoDB
Relational vs NoSQL, replicas, caching.
A relational database works like a filing cabinet full of cross-referenced folders. Pull one record and it points you at ten related ones, and the cabinet itself enforces the rules about what is allowed to go where. DynamoDB works like a coat check. You hand over a numbered ticket and get exactly one item back in milliseconds, whether the venue is holding fifty coats or fifty million. Neither one is *better*. They answer different questions, and the SAA (Solutions Architect Associate) exam loves asking which question you are really answering. In this lesson you provision both from the aws CLI (command-line interface), wire up the levers for staying available and for scaling reads, and meet the caching and warehousing services that round out the family.
RDS hands you a database, not a server to babysit
Amazon RDS (Relational Database Service) runs a real database engine for you: PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, or IBM Db2, on instances that AWS operates. You still own the schema, the queries, and the indexes. AWS owns the endless half nobody enjoys: provisioning, patching the operating system, taking backups, and keeping the failover plumbing alive. Two ideas in the create call below carry most of the design weight. --multi-az gives you a standby, a copy living in a second AZ (Availability Zone, a physically separate set of data centres inside the same region) that receives every write at the same moment as the primary and takes over on its own if the primary or its whole AZ dies. A read replica is a different animal with its own command. It is a copy that receives writes *after the fact*, has its own address, and exists so you can aim SELECT traffic at it on purpose. Same word, replica. Opposite jobs. The next section pulls them apart.
# Multi-AZ Postgres in private subnets, encrypted at rest,# master password generated and held by Secrets Manageraws rds create-db-instance \--db-instance-identifier app-db \--engine postgres --engine-version 17.4 \--db-instance-class db.t4g.medium \--allocated-storage 100 --storage-type gp3 \--multi-az \--db-subnet-group-name private-db-subnets \--vpc-security-group-ids sg-0a1b2c3d4e5f67890 \--master-username dbadmin --manage-master-user-password \--storage-encrypted --backup-retention-period 7 \--no-publicly-accessible{"DBInstance": {"DBInstanceIdentifier": "app-db","DBInstanceStatus": "creating","Engine": "postgres","MultiAZ": true,"StorageEncrypted": true,"PubliclyAccessible": false,"MasterUserSecret": {"SecretArn": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:rds!db-3f8e91c2-7a44-4d2e-9c1a-0b5c6d7e8f90-Ab12Cd","SecretStatus": "creating"}}}# ~10 min later: grab the endpoint your application connects toaws rds describe-db-instances --db-instance-identifier app-db \--query 'DBInstances[0].[DBInstanceStatus,Endpoint.Address,MultiAZ]' --output table# | available | app-db.cxk3mp0d1r2s.eu-west-1.rds.amazonaws.com | True |
Every flag in that call is a decision you would otherwise make badly at 2am. --db-subnet-group-name pins the instance to private subnets, because a database has no business holding a public IP (Internet Protocol) address, and --no-publicly-accessible says so out loud. --storage-encrypted turns on encryption at rest through KMS (Key Management Service, the AWS key store). You *cannot* switch that on later without taking a snapshot, copying it encrypted, and migrating onto the copy, so choose it on day one. --manage-master-user-password skips the plaintext password altogether. RDS generates one, parks it in Secrets Manager, and rotates it every seven days by default, so the credential never lands in your shell history or your Terraform state. One sharp edge: RDS refuses to create a read replica from an instance whose password it manages this way, with SQL Server the lone exception. That is why you hand password management back before scaling out reads below.
Multi-AZ keeps you alive; read replicas keep you fast
This is the distinction the exam tests harder than any other, so get it into your bones. The Multi-AZ standby copies synchronously: the primary does not tell your application "saved" until the standby has the write too. A failover therefore loses nothing that was committed. The price is that you can *never read from the standby*. It sits there, fully paid for, waiting. Failover works by repointing the instance's DNS (Domain Name System, the internet's address book) record at the standby, usually inside 60 to 120 seconds, and your application keeps using the same endpoint name throughout. Read replicas copy asynchronously: the primary acknowledges the write straight away and ships it onward afterwards. Each replica gets its own endpoint, may trail the primary by milliseconds or by minutes, and can sit in a different region entirely. MySQL, MariaDB, and PostgreSQL instances take up to 15 of them. Any replica can be *promoted* into a standalone database, which is exactly what turns a cross-region replica into a disaster-recovery building block. One newer option to keep in your back pocket: a Multi-AZ DB cluster runs one writer plus *two standbys you can actually read from*, giving you both benefits at a higher bill.
# Prep: RDS can't create a replica while it manages the master password# in Secrets Manager — switch app-db to a self-managed password firstaws rds modify-db-instance --db-instance-identifier app-db \--master-user-password "$DB_PASSWORD"# Asynchronous read replica: its own endpoint, no failover roleaws rds create-db-instance-read-replica \--db-instance-identifier app-db-ro-1 \--source-db-instance-identifier app-db \--db-instance-class db.t4g.medium{"DBInstance": {"DBInstanceIdentifier": "app-db-ro-1","DBInstanceStatus": "creating","ReadReplicaSourceDBInstanceIdentifier": "app-db","MultiAZ": false}}# How far behind is it? This is the lag you must design around.aws cloudwatch get-metric-statistics --namespace AWS/RDS \--metric-name ReplicaLag --statistics Average --period 60 \--dimensions Name=DBInstanceIdentifier,Value=app-db-ro-1 \--start-time 2026-07-13T09:00:00Z --end-time 2026-07-13T09:05:00Z# "Datapoints": [ { "Average": 0.42, "Unit": "Seconds" } ]
ReplicaLag CloudWatch metric, and never park a replica behind an endpoint your checkout flow depends on.Aurora: same engines, storage rebuilt from scratch
Amazon Aurora speaks MySQL and PostgreSQL on the wire, so your drivers and your SQL (Structured Query Language) keep working untouched. What changes is the warehouse behind the counter. A normal database writes whole data pages down onto one disk volume. An Aurora instance sends only its redo log records, the short "here is what changed" notes a database writes before applying the change itself, to a shared storage layer that keeps six copies across three Availability Zones and can lose two of them without losing a write. That design explains why Aurora's numbers beat standard RDS. Storage grows on its own up to 128 TiB (tebibytes, roughly 140 terabytes). Up to 15 replicas read from the *same shared volume*, so lag normally sits well under 100 milliseconds instead of seconds. Failover promotes a replica in under 30 seconds most of the time, because there is no backlog for the new primary to catch up on. Your application talks to two names: the writer endpoint, which always follows whichever instance is primary right now, and the reader endpoint, which spreads connections across the replicas. Two variants show up on the exam. Aurora Serverless v2 scales compute in small steps called ACUs (Aurora Capacity Units), all the way down to zero when nothing is happening, then wakes back up on demand, which suits spiky traffic and dev environments. Aurora Global Database copies the storage layer into other regions with lag typically under a second, giving you DR (disaster recovery) figures plain RDS cannot touch: an RPO (recovery point objective, how much data you can afford to lose) of about one second and an RTO (recovery time objective, how long you can afford to be down) usually under a minute. The catch is the bill, roughly 20% above equivalent RDS instances by the commonly quoted figure, and you get only the two engine flavors.
DynamoDB: a coat check the size of a region
Amazon DynamoDB is a serverless key-value and document database. No instances, no patching, no connection pools. You create a table and call an API (application programming interface, where your code sends ordinary HTTPS web requests and gets answers back). Underneath, DynamoDB runs each item's partition key through a hash function to decide which physical partition holds it, then splits partitions on its own as data or traffic grows. That is the entire scaling story, and it is also the entire constraint. Every partition gets a bounded slice of throughput. A key with low *cardinality* (the count of distinct values it can hold) like Country funnels the whole crowd onto one *hot partition* and starts throttling, while a high-cardinality key like UserId spreads the same load thinly. Get the key right and reads come back in single-digit milliseconds at any size. Billing goes one of two ways: per request (PAY_PER_REQUEST, the right call for spiky or unknown traffic) or per unit of provisioned capacity (cheaper once the load is steady). Reads are eventually consistent by default, meaning they can briefly miss the newest write, and they cost half of a strongly consistent read, which the CLI asks for with --consistent-read.
# Serverless table: no capacity to size, billed per requestaws dynamodb create-table \--table-name Sessions \--attribute-definitions AttributeName=UserId,AttributeType=S \AttributeName=SessionId,AttributeType=S \--key-schema AttributeName=UserId,KeyType=HASH \AttributeName=SessionId,KeyType=RANGE \--billing-mode PAY_PER_REQUEST# "TableStatus": "CREATING" → ACTIVE within seconds# Write an item, then read it back with strong consistencyaws dynamodb put-item --table-name Sessions \--item '{"UserId":{"S":"u-1042"},"SessionId":{"S":"s-9001"},"Expires":{"N":"1786579200"}}'aws dynamodb get-item --table-name Sessions \--key '{"UserId":{"S":"u-1042"},"SessionId":{"S":"s-9001"}}' \--consistent-read --return-consumed-capacity TOTAL{"Item": {"UserId": {"S": "u-1042"},"SessionId": {"S": "s-9001"},"Expires": {"N": "1786579200"}},"ConsumedCapacity": { "TableName": "Sessions", "CapacityUnits": 1.0 }}
The trade-offs are sharp. One item cannot exceed 400 KB (kilobytes). There are no joins. You can query efficiently only by the table's keys, or by a secondary index you define alongside them for a second query shape. That turns relational design on its head. You write down how the application will read the data first, then build the table around those reads. In exchange you get a database with no maintenance window and the same latency whether it serves zero requests a second or several million.
Caches, warehouses, and choosing between them
Two more services finish the exam picture. Amazon ElastiCache runs managed in-memory stores (Valkey, Redis OSS, or Memcached) that sit in front of any database like a notepad on the desk beside the filing cabinet. Repeated reads get answered from memory in microseconds, and a cache node costs a fraction of another read replica, because a replica bills as a whole database instance. DynamoDB has its own purpose-built version, DAX (DynamoDB Accelerator), a write-through cache that drops read latency from milliseconds to microseconds. It speaks the DynamoDB API, so you swap in the DAX client and leave your queries alone. And when the question is analytics, scanning terabytes to answer something like "what did each region earn last quarter", a transactional database is the wrong shape entirely. Amazon Redshift is the columnar data warehouse AWS built for that job.
The habit worth building as an architect: ask how the data will be read before you pick the engine, then reach for the matching lever. Multi-AZ to survive a failure. Replicas and caches to survive read volume. Partition-key design to survive DynamoDB's throughput limits. The next lesson, High availability & DR, pulls the camera back from single databases to whole architectures: what RPO and RTO mean once you have to write them into a contract, how backup-and-restore stacks up against pilot-light and active-active, and what an outage is actually allowed to cost you.
Multi-AZ RDS is a synchronous standby built for failover. It is never a read scaler, whatever an exam option tries to tell you. Read replicas are asynchronous and can lag, which makes them good for read traffic and wrong as your only availability story if you pretend they are always in step.
DynamoDB asks you to know your access patterns before you create the table: how many distinct values your partition key holds, how big your items get, and whether you need transactions or strongly consistent reads. Single-table design is a choice you can take or leave. Hot partitions are not.
Try this
Check the Multi-AZ flag on an RDS instance, then list your DynamoDB tables and see how each one is billed. Run this against lab resources only.
aws rds describe-db-instances --query 'DBInstances[].{Id:DBInstanceIdentifier,Engine:Engine,MultiAZ:MultiAZ,Class:DBInstanceClass}' --output tableaws dynamodb list-tables --output tableaws dynamodb describe-table --table-name orders \--query '{Table:TableName,Billing:BillingModeSummary.BillingMode,Keys:KeySchema}' --output json
-------------------------------------------------| DescribeDBInstances |+--------+----------+----------+----------------+| Class | Engine | MultiAZ | Id |+--------+----------+----------+----------------+| db.t3.micro | postgres | True | lab-pg |+--------+----------+----------+----------------+orders{"Table":"orders","Billing":"PAY_PER_REQUEST","Keys":[{"AttributeName":"pk","KeyType":"HASH"}]}
Takeaway
Three lines to keep in your head: Multi-AZ buys availability, read replicas buy read scale, and DynamoDB buys predictable single-digit-millisecond reads for as long as your keys match the way you query.
Next: take one path through an application you know and decide, in a single paragraph, whether it wants a relational database or DynamoDB. Name the failover behavior and the consistency guarantee you are signing up for.