Now that compute can scale and survive (Post 03), the next question is where the data lives. AWS offers a long catalogue of managed data services. This post focuses on the four most common: RDS (managed relational), Aurora (RDS's cloud-native superset), DynamoDB (managed NoSQL key-value / document), and ElastiCache (managed Redis / Memcached). For each: when to choose it, the high-availability patterns, the backup story, and the most common cost surprises. A quick comparison to S3, Redshift, OpenSearch, and Neptune appears at the end so the decision tree is complete.

Series map: 00 Foundations → 01 Network First → 02 Compute & Storage → 03 Scale & Survive → 04 Managed Data (this post) → 05 Serverless & Events → 06 Workflow Orchestration → 07 Observability & Security → 08 IaC.


Learning Outcome

  • Choose between RDS, Aurora, DynamoDB, ElastiCache for a given workload.
  • Configure RDS Multi-AZ for HA and read replicas for read scaling.
  • Use Aurora's storage layer and serverless V2 mode.
  • Design a DynamoDB table with a sensible partition key + sort key + secondary indexes.
  • Configure DynamoDB capacity mode (on-demand vs provisioned + auto-scaling).
  • Place caching tiers correctly.
  • Recognise the major cost levers per service.

Chapter 1 — Pick the right data store

Quick decision tree:

1
2
3
4
5
6
7
8
9
10
11
12
Do you need SQL + ACID transactions across rows?
├── Yes
│ ├── Open-source engine compatibility matters? → RDS (Postgres/MySQL/MariaDB/etc.)
│ └── Cloud-native performance + scale matters? → Aurora (Postgres or MySQL compatible)
└── No
├── Key-value / document, single-row ops? → DynamoDB
├── Hot in-memory cache? → ElastiCache (Redis or Memcached)
├── Object/file blobs? → S3 (Post 02)
├── Analytics on TB-PB scale? → Redshift / Athena
├── Full-text / log search? → OpenSearch
└── Graph? → Neptune

The biggest mistake is choosing by familiarity rather than fit. Postgres on RDS is a fine default; DynamoDB is a much better fit for high-write key-value patterns. Picking wrong locks you into a costly migration later.


Chapter 2 — RDS

RDS (Relational Database Service) runs PostgreSQL, MySQL, MariaDB, Oracle, or SQL Server on EC2 + EBS, managed by AWS. You get:

  • Daily automated backups + point-in-time recovery (1-35 days).
  • Multi-AZ failover (synchronous replica in another AZ; automatic promotion).
  • Up to 15 read replicas for read scaling.
  • Maintenance windows for patching.
  • IAM authentication, KMS encryption at rest, TLS in transit.

What you give up: full OS access. You can't run sudo on an RDS host or install custom Postgres extensions outside AWS's approved list.

Engines and versions

Engine Typical use
Postgres most modern apps; richest feature set
MySQL legacy or compatibility
MariaDB MySQL drop-in
Oracle / SQL Server licensed workloads

Pick the most recent LTS major version for production unless a specific older feature is needed.

Multi-AZ vs read replica

Feature Multi-AZ Read replica
Purpose HA: automatic failover Read scaling
Replication synchronous asynchronous
Visible? invisible standby endpoint queryable
Failover time 60-120 sec manual promote
Cost 2× the instance 1× per replica

Production = always Multi-AZ. Heavy-read workloads = also one or more read replicas.

Backups

  • Automated snapshots — every day, retained 1-35 days, free for active DBs.
  • Manual snapshots — kept until you delete them.
  • Point-in-time restore — recover to any second within the retention window (writes go to transaction logs in S3).

Trick: RDS lets you spin up a new DB from a snapshot in seconds for testing. Great for "show me prod data from yesterday in a sandbox" workflows. Just remember to delete it after.


Chapter 3 — Aurora

Aurora is AWS's cloud-native rewrite of MySQL and PostgreSQL. Compatible at the wire level, but with a custom storage engine that:

  • Replicates data across 6 copies in 3 AZs automatically.
  • Keeps working for writes after losing up to 2 copies, and for reads after losing up to 3.
  • Auto-grows storage in 10 GB chunks up to 128 TB.
  • Provides up to 15 read replicas that share the same storage layer (lag in single-digit ms).

RDS with a synchronous standby and an asynchronous read replica, next to Aurora instances sharing one cluster volume with six copies across three AZs

Aurora vs RDS

Trait RDS Aurora
Engines many Postgres / MySQL compatible only
Storage EBS (provisioned) shared cluster volume, auto-grow
Throughput vs vanilla baseline up to 3-5× MySQL, 2-3× Postgres
Cost cheaper at small scale costlier baseline, cheaper at scale
Best for low-traffic apps, small teams production OLTP at scale, SaaS

Aurora Serverless V2

Aurora Serverless V2 scales the compute capacity (ACU — Aurora Capacity Units) up and down with load, in fractions of an ACU. Used to be best for unpredictable workloads; in 2026 it's mature enough to be a viable default for new Aurora clusters.


Chapter 4 — DynamoDB

DynamoDB is a fully managed NoSQL key-value + document store. Single-digit millisecond latency at any scale. No servers to provision, no AZ design — AWS handles all of it.

Trade-offs vs SQL:

  • No joins (denormalise instead).
  • Limited transactional support (single-region transactions OK; multi-region eventual).
  • Query patterns must be designed first — DynamoDB tables are not "queried like SQL"; you access by key.

Table schema

A table has:

  • A partition key (mandatory) — determines which physical partition stores the item.
  • An optional sort key — orders items within a partition.
  • Items are JSON-like documents with arbitrary attributes.
  • Secondary indexes (Global or Local) let you query by other attributes.
1
2
3
4
5
6
7
8
9
10
11
# write
table.put_item(Item={"user_id": "u123", "ts": 1700000000, "action": "login"})

# strongly-consistent get by key
table.get_item(Key={"user_id": "u123", "ts": 1700000000})

# query by partition key + sort key range
table.query(
KeyConditionExpression=Key("user_id").eq("u123") & Key("ts").between(t_start, t_end)
)

Capacity modes

Mode When
On-demand unpredictable traffic, dev, low-traffic
Provisioned + auto-scaling steady traffic with daily curve

Start with on-demand; switch to provisioned once you have a stable traffic profile and the cost gap matters.

Streams + change capture

DynamoDB Streams publishes every item-level change for 24 hours. Wire to Lambda for:

  • replication to another store (S3, OpenSearch, another region),
  • audit trails,
  • triggering downstream workflows.

Single-table design

Power users put multiple entity types in one table with carefully designed key patterns. Higher learning curve but lower cost and faster queries. Worth reading Alex DeBrie's The DynamoDB Book when ready.


Chapter 5 — ElastiCache

ElastiCache runs Redis or Memcached as a managed service. Two flavours:

Engine Best for
Redis (ElastiCache for Redis) rich data structures, pub/sub, persistence, replicas, cluster mode
Memcached pure cache, multi-threaded, simpler

Modern AWS also offers MemoryDB for Redis — Redis-compatible with multi-AZ durable storage (effectively Redis-as-a-database). Use when you need Redis semantics plus durability.

Cache patterns

  • Cache-aside (lazy loading) — app reads from cache first; on miss, query DB and populate.
  • Write-through — every DB write also updates the cache.
  • TTL eviction — auto-expire entries after a duration.
  • Session store — sticky-session alternative for stateless app servers.
  • Rate limiting / leaderboards — natural Redis use cases.

Trick: when adding a cache to fix a slow query, set a short TTL (60-300 sec) at first. Lets you measure hit rate and roll back without stale-data risk. Expand TTL once you're confident the data tolerates it.


Chapter 6 — Choosing between RDS, Aurora, DynamoDB

Scenario Pick
Small SaaS, < 1k req/s, SQL RDS Postgres Multi-AZ
Larger SaaS, 5k+ req/s, SQL Aurora Postgres
Mobile app backend, key-value DynamoDB on-demand
User-session store ElastiCache Redis
Time-series IoT writes DynamoDB or TimeStream
Reporting / analytics Redshift or S3 + Athena
Search OpenSearch
Caching API responses ElastiCache Redis

There is no universal best. Match the access pattern.


Chapter 7 — Costs and gotchas

Item Typical cost driver Saver
RDS / Aurora instance per-hour by class reserved instance / 1-3 yr commit
RDS storage per GB-month gp3 rightsize; gp3 not gp2
RDS backup beyond DB size per GB-month shorter retention or move old to S3
RDS Multi-AZ 2× the instance required for production
DynamoDB on-demand per request (read + write units) provisioned + auto-scale at scale
DynamoDB storage per GB-month TTL old items
DynamoDB GSI extra reads/writes only create needed indexes
ElastiCache nodes per-hour by class reserved nodes; right-size
Cross-AZ traffic per GB keep app + DB in same AZ where safe

Watch out: turning on performance insights, enhanced monitoring, automated backups at long retention, and many secondary indexes adds line items that double a small DB's bill quietly.


Chapter 8 — Migration shortcuts

  • DMS (Database Migration Service) — continuous replication from one DB to another (on-prem → RDS, MySQL → Aurora, RDS → DynamoDB).
  • AWS Schema Conversion Tool (SCT) — convert schemas + stored procedures between engines.
  • pg_dump / mysqldump still work fine for small one-shot loads.

For production cut-overs, the DMS "ongoing-replication then cut" pattern is the safe playbook.


Practice Exercises

Exercise 1 — RDS Postgres Multi-AZ

Create an RDS Postgres db.t4g.micro Multi-AZ in the isolated subnets of the VPC from Post 01. Confirm:

  • the standby in another AZ exists,
  • the master endpoint resolves to the active node,
  • a forced failover (reboot-db-instance --force-failover) flips traffic within 60-120 sec.

Exercise 2 — Read replica

Add a read replica to the Exercise 1 DB. Point a read-only client (e.g., psql --readonly) at the replica endpoint. Verify writes fail on the replica, succeed on the primary.

Exercise 3 — DynamoDB design

Design a DynamoDB table for a simple URL shortener:

  • partition key: short_code (string)
  • attributes: long_url, created_at, hits
  • secondary requirement: list 100 most recent short codes

What partition/sort key and (if needed) GSI satisfies both access patterns?

One valid answer
  • Table partition key: short_code (for direct lookup).
  • GSI: partition key gsi_pk (always literal "ALL"), sort key created_at. Query the GSI with pk = "ALL", sorted descending, limit 100.

Note the GSI pattern: cramming everything into one partition is fine at low scale but won't scale to massive write throughput; that's where you'd shard gsi_pk (e.g., by date).

Exercise 4 — Cache-aside

Add ElastiCache Redis in front of the RDS from Exercise 1. Implement a simple Python service that:

  • on GET /user/{id} checks Redis first,
  • on miss, queries Postgres, populates Redis with TTL 60 sec,
  • on POST /user/{id} writes Postgres and invalidates the Redis key.

Measure hit-rate after warm-up.

Exercise 5 — DynamoDB Streams to Lambda

Enable Streams on the table from Exercise 3. Hook up a Lambda that logs every change. Confirm in CloudWatch Logs.


Cheat-sheet

What How
Default SQL RDS Postgres Multi-AZ
SQL at scale Aurora Postgres / MySQL
Key-value at scale DynamoDB on-demand
Hot cache ElastiCache Redis
Multi-AZ RDS always for production
Read replicas up to 15; async lag
RDS backup window retain 1-35 days; PITR auto
Aurora storage shared, auto-grows, 6 copies / 3 AZ
Aurora Serverless V2 new clusters default candidate
DynamoDB key design partition + optional sort
DynamoDB query model by key, not arbitrary SQL
Streams 24-hour change log → Lambda
Cache pattern lazy load + TTL
Cost saver RI for steady DBs, on-demand for spikes

What's next

Post 05 introduces serverless and event-driven AWS — Lambda, API Gateway, SQS, SNS, EventBridge — the glue that connects everything from Posts 02-04 into asynchronous, scalable architectures.


References

Note: this post is in progress. Planned additions: Aurora Global Database walkthrough, DynamoDB single-table design example, OpenSearch + Redshift overview.