AWS Storage & Databases
S3, RDS, Aurora, ElastiCache, DynamoDB — the data layer every backend engineer uses.
S3 (Simple Storage Service)
S3 is object storage — flat namespace of buckets containing objects (key/value pairs). Not a filesystem; no directories, just key prefixes that look like paths.
Storage Classes
graph LR
classDef blue fill:#3498db,stroke:#2980b9,color:#fff
classDef teal fill:#1abc9c,stroke:#16a085,color:#fff
classDef orange fill:#e67e22,stroke:#d35400,color:#fff
classDef yellow fill:#f39c12,stroke:#d68910,color:#000
classDef dark fill:#2c3e50,stroke:#1a252f,color:#fff
HOT["Standard<br/>$0.023/GB"]:::blue
SMART["Intelligent-Tiering<br/>auto-tier"]:::teal
IA["Standard-IA<br/>$0.0125/GB"]:::orange
ZONE["One Zone-IA<br/>$0.01/GB"]:::yellow
GLACIER["Glacier Instant<br/>$0.004/GB"]:::dark
DEEP["Glacier Deep Archive<br/>$0.00099/GB"]:::dark
HOT --> SMART --> IA --> ZONE --> GLACIER --> DEEP
| Class | Price/GB | Retrieval | Use |
|---|---|---|---|
| Standard | $0.023 | ms | Frequent access |
| Intelligent-Tiering | varies | ms | Unknown patterns |
| Standard-IA | $0.0125 | ms + $0.01/GB fee | Infrequent, 30-day min |
| One Zone-IA | $0.01 | ms + fee | Reproducible data only |
| Glacier Instant | $0.004 | ms | Archive, fast retrieval |
| Glacier Deep Archive | $0.00099 | 12 hr | Cheapest, long-term |
Lifecycle policy automates transitions:
{
"Rules": [{
"Status": "Enabled",
"Transitions": [
{ "Days": 30, "StorageClass": "STANDARD_IA" },
{ "Days": 90, "StorageClass": "GLACIER_IR" },
{ "Days": 365, "StorageClass": "DEEP_ARCHIVE" }
],
"Expiration": { "Days": 2555 }
}]
}
Versioning & Replication
Versioning: Once enabled on a bucket, every PutObject creates a new version. Delete adds a delete marker (object recoverable). Can't disable — only suspend.
Cross-Region Replication (CRR): Async replication to a bucket in another region. Requires versioning on both buckets. Use for: DR, latency (serve from nearest region), compliance (data residency).
Source: us-east-1 bucket (versioning on)
↓ async CRR rule
Destination: eu-west-1 bucket (versioning on)
Same-Region Replication (SRR): Same region, different account or bucket. Use for log aggregation, test/prod data copies.
Access Control
Priority order (most → least specific):
- S3 Block Public Access (account/bucket level — overrides everything)
- Bucket Policy (resource-based, JSON)
- ACLs (legacy, avoid for new setups — AWS recommends disabling ACLs)
- IAM policy (identity-based)
For cross-account access: both the IAM policy on the caller AND the bucket policy must allow. One alone is insufficient.
// Bucket policy: allow specific IAM role from another account
{
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::OTHER-ACCOUNT:role/reader-role" },
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}]
}
Presigned URLs
Temporary access to private objects — no IAM credentials required by the recipient:
aws s3 presign s3://my-bucket/report.pdf --expires-in 3600
# Returns: https://my-bucket.s3.amazonaws.com/report.pdf?X-Amz-Signature=...
The URL is signed with the IAM identity's credentials. If the identity's permissions are revoked, existing presigned URLs stop working.
S3 Event Notifications
graph LR
classDef blue fill:#3498db,stroke:#2980b9,color:#fff
classDef orange fill:#e67e22,stroke:#d35400,color:#fff
classDef green fill:#2ecc71,stroke:#27ae60,color:#fff
S3["S3: PutObject / DeleteObject"]:::blue
SQS["SQS Queue"]:::orange
SNS["SNS Topic"]:::orange
LAMBDA["Lambda Function"]:::green
EB["EventBridge"]:::blue
S3 -->|event filter by prefix/suffix| SQS
S3 --> SNS
S3 --> LAMBDA
S3 --> EB
EventBridge is the most flexible — fan-out to many targets, content filtering, cross-account delivery.
Multipart Upload
Large files (> 100MB) should use multipart upload — parallel parts, resume on failure:
# AWS CLI handles multipart automatically for files > 8MB
aws s3 cp large-file.tar.gz s3://my-bucket/ --storage-class STANDARD
# Manual multipart (for SDK control)
aws s3api create-multipart-upload --bucket my-bucket --key large-file.tar.gz
# Returns: UploadId
# Upload parts in parallel (each part min 5MB except last)
aws s3api upload-part --bucket my-bucket --key large-file.tar.gz \
--part-number 1 --upload-id <UploadId> --body part1.bin
# Complete (specify all ETags from upload-part responses)
aws s3api complete-multipart-upload --bucket my-bucket --key large-file.tar.gz \
--upload-id <UploadId> --multipart-upload '{"Parts":[{"PartNumber":1,"ETag":"..."}]}'
# Abort (cleanup incomplete uploads — run periodically or set lifecycle rule)
aws s3api abort-multipart-upload --bucket my-bucket --key large-file.tar.gz --upload-id <id>
Lifecycle rule to clean up incomplete multiparts:
{"Rules": [{"Status":"Enabled","AbortIncompleteMultipartUpload":{"DaysAfterInitiation":7}}]}
Object Lock (WORM)
Write-Once-Read-Many — objects cannot be deleted or overwritten for a retention period. Required for compliance (SEC 17a-4, HIPAA).
# Enable Object Lock at bucket creation (cannot add to existing bucket)
aws s3api create-bucket --bucket compliance-logs --object-lock-enabled-for-bucket
# Apply retention to an object
aws s3api put-object-retention \
--bucket compliance-logs \
--key audit-2024-01-15.log \
--retention '{"Mode":"COMPLIANCE","RetainUntilDate":"2031-01-15T00:00:00Z"}'
# COMPLIANCE mode: even root cannot delete before retain date
# GOVERNANCE mode: users with s3:BypassGovernanceRetention CAN delete
S3 Select — Query Inside Objects
Query CSV/JSON/Parquet objects with SQL without downloading the whole file:
aws s3api select-object-content \
--bucket my-bucket \
--key logs/2024/01/15.csv.gz \
--expression "SELECT * FROM S3Object WHERE status = '500'" \
--expression-type SQL \
--input-serialization '{"CSV":{"FileHeaderInfo":"USE"},"CompressionType":"GZIP"}' \
--output-serialization '{"CSV":{}}' \
/dev/stdout
# Only the matching rows are transferred — saves bandwidth + cost vs downloading all
S3 Performance
Prefix-based parallelism:
Single prefix: ~3500 PUT/s, ~5500 GET/s
Multiple prefixes: scales linearly
Bad: all files at root (one prefix = bottleneck)
Good: date/hash prefix: 2024/01/15/abc123.parquet (many prefixes)
Transfer Acceleration:
Routes uploads through CloudFront edge locations
Useful when uploading from far geographies (EU → us-east-1)
Enable per bucket, use: mybucket.s3-accelerate.amazonaws.com
~50% faster uploads from distant regions
Byte-Range Fetches:
Download parts of large files in parallel
GET /myfile?Range: bytes=0-1048576 # first 1MB
Application assembles parts → faster for large downloads
RDS & Aurora
RDS Multi-AZ vs Read Replicas
These solve different problems. They're not interchangeable.
graph TD
classDef blue fill:#3498db,stroke:#2980b9,color:#fff
classDef green fill:#2ecc71,stroke:#27ae60,color:#fff
classDef orange fill:#e67e22,stroke:#d35400,color:#fff
classDef red fill:#e74c3c,stroke:#c0392b,color:#fff
classDef teal fill:#1abc9c,stroke:#16a085,color:#fff
subgraph MultiAZ["Multi-AZ: HA / Failover"]
PRIMARY["Primary DB (AZ-a): reads + writes"]:::blue
STANDBY["Standby DB (AZ-b): synchronous replica"]:::orange
PRIMARY -->|"synchronous replication<br/>(data loss = 0)"| STANDBY
STANDBY -.->|"no traffic — failover only ~60-120s"| WRITER_NEW["New Primary (promoted)"]:::green
end
subgraph ReadReplica["Read Replicas: scale reads"]
MASTER["Primary DB: writes"]:::blue
RR1["Read Replica 1 (same region)"]:::green
RR2["Read Replica 2 (cross-region)"]:::teal
MASTER -->|"async replication<br/>(slight lag)"| RR1
MASTER -->|"async replication"| RR2
end
| Multi-AZ | Read Replica | |
|---|---|---|
| Purpose | High availability / failover | Read scalability |
| Replication | Synchronous | Asynchronous (lag possible) |
| Serves traffic | Standby serves NO traffic | Yes — reads only |
| Failover | Automatic (~60-120s) | Manual promotion |
| Cross-region | No (single region) | Yes |
| Cost | 2x instance cost | Additional instance cost |
You can have BOTH: Multi-AZ for HA + Read Replicas for scale. They're orthogonal.
Aurora Architecture
Aurora is not just "managed MySQL/PostgreSQL" — it's a storage-compute separation architecture.
graph TD
classDef blue fill:#3498db,stroke:#2980b9,color:#fff
classDef green fill:#2ecc71,stroke:#27ae60,color:#fff
classDef orange fill:#e67e22,stroke:#d35400,color:#fff
classDef teal fill:#1abc9c,stroke:#16a085,color:#fff
subgraph Compute["Compute Layer"]
WRITER["Writer instance (1)"]:::blue
R1["Reader instance 1"]:::green
R2["Reader instance 2"]:::green
end
subgraph Storage["Aurora Distributed Storage: 6 copies across 3 AZs"]
S1["AZ-a copy 1"]:::orange
S2["AZ-a copy 2"]:::orange
S3["AZ-b copy 1"]:::orange
S4["AZ-b copy 2"]:::orange
S5["AZ-c copy 1"]:::orange
S6["AZ-c copy 2"]:::orange
end
WRITER -->|write quorum: 4/6| Storage
R1 -->|read from storage| Storage
R2 -->|read from storage| Storage
WRITER -.->|"failover ~30s<br/>new writer from reader"| R1
Key properties:
- Storage auto-grows in 10 GB increments up to 128 TB — no provisioning
- 6 copies across 3 AZs — can lose an AZ and still write (only needs 4/6 for write quorum, 3/6 for read)
- Readers share the same storage — no replication lag on reads (reads are consistent)
- Failover is faster (~30s) because a reader is already connected to the same storage
Aurora Serverless v2
Scales compute (ACUs — Aurora Capacity Units) instantly in fine-grained increments. Unlike v1 which had cold starts, v2 scales continuously.
Minimum: 0.5 ACU (1 ACU ≈ 2 GB RAM)
Maximum: 128 ACU
Scale up: within seconds on demand
Scale to zero: yes (v2 can pause)
Good for: dev/test environments, irregular workloads, unpredictable spikes.
RDS Proxy
Connection pooler as a managed service — sits between your app and RDS/Aurora.
Problem it solves: Serverless functions (Lambda) open new DB connections per invocation. RDS can handle ~5000 connections max. 1000 concurrent Lambdas = 1000 connections, overwhelming the DB.
Lambda (1000 concurrent) → RDS Proxy (maintains ~50 pooled connections) → RDS
Also provides: IAM authentication for DB connections, automatic failover (no connection string change needed), TLS enforcement.
ElastiCache
Managed in-memory caching. Two engines:
| Redis | Memcached | |
|---|---|---|
| Data structures | Strings, hashes, lists, sets, sorted sets, streams | Strings only |
| Persistence | Yes (RDB snapshots + AOF) | No |
| Replication | Yes (primary + replicas) | No |
| Cluster mode | Yes (sharding across nodes) | Yes (hash-based) |
| Pub/Sub | Yes | No |
| Transactions / Lua | Yes | No |
| Use case | Sessions, leaderboards, rate limiting, queues | Simple KV cache, high-throughput read cache |
Rule: Use Redis unless you specifically need Memcached's multi-threaded simplicity or are already on Memcached.
Redis Cluster Mode
graph TD
classDef blue fill:#3498db,stroke:#2980b9,color:#fff
classDef orange fill:#e67e22,stroke:#d35400,color:#fff
classDef green fill:#2ecc71,stroke:#27ae60,color:#fff
subgraph Shard1["Shard 1: slots 0-5460"]
P1["Primary (AZ-a)"]:::blue --> R1A["Replica (AZ-b)"]:::orange
P1 --> R1B["Replica (AZ-c)"]:::green
end
subgraph Shard2["Shard 2: slots 5461-10922"]
P2["Primary (AZ-b)"]:::blue --> R2A["Replica (AZ-a)"]:::orange
end
subgraph Shard3["Shard 3: slots 10923-16383"]
P3["Primary (AZ-c)"]:::blue --> R3A["Replica (AZ-a)"]:::orange
end
16384 hash slots divided across shards. Key k maps to slot CRC16(k) % 16384. Client connects to any node and gets redirected.
Cache-Aside Pattern (Lazy Loading)
func getUser(id string) (*User, error) {
// 1. Check cache
if cached, err := redis.Get(ctx, "user:"+id).Result(); err == nil {
return unmarshal(cached), nil
}
// 2. Cache miss → DB
user, err := db.QueryUser(id)
if err != nil { return nil, err }
// 3. Populate cache, set TTL
redis.Set(ctx, "user:"+id, marshal(user), 5*time.Minute)
return user, nil
}
Write-through (update cache on every write) prevents stale reads but wastes memory caching rarely-read data. Cache-aside is more common.
DynamoDB
Fully managed serverless NoSQL — key-value and document model. No servers, no connections, auto-scales.
Data Model
Every table has a primary key:
- Simple primary key: Partition key only — each item uniquely identified by partition key
- Composite primary key: Partition key + Sort key — items with same PK are sorted by SK, enabling range queries
Table: Orders
PK: customerId (partition key)
SK: orderId#timestamp (sort key)
Query: all orders for customer "123", sorted by time
KeyConditionExpression: customerId = "123" AND begins_with(SK, "order#")
Read/Write Capacity
| Mode | When to use |
|---|---|
| Provisioned | Predictable traffic; set RCU/WCU; can use Auto Scaling or reserved capacity |
| On-demand | Unpredictable spikes; pay per request; ~6x more expensive at steady load |
RCU/WCU: 1 RCU = 1 strongly consistent read of 4 KB/s (or 2 eventually consistent reads). 1 WCU = 1 write of 1 KB/s.
GSI (Global Secondary Index)
Query on non-key attributes — projects a subset of table data with a different partition key.
Table: Orders (PK: customerId, SK: orderId)
GSI: OrdersByStatus
PK: status ← now queryable
SK: createdAt
Projection: ALL
Query: all PENDING orders sorted by time
→ query GSI with status = "PENDING"
GSIs have their own RCU/WCU capacity and can diverge from the main table during heavy writes (eventual consistency).
DynamoDB Streams + Lambda (Event-driven)
graph LR
classDef blue fill:#3498db,stroke:#2980b9,color:#fff
classDef orange fill:#e67e22,stroke:#d35400,color:#fff
classDef green fill:#2ecc71,stroke:#27ae60,color:#fff
DDB["DynamoDB Table"]:::blue -->|"change stream<br/>(INSERT/MODIFY/REMOVE)"| STREAM["DynamoDB Stream"]:::orange
STREAM -->|trigger| LAMBDA["Lambda Function"]:::green
LAMBDA -->|"fan-out"| ES["OpenSearch: search index"]:::blue
LAMBDA --> SQS["SQS: async processing"]:::orange
LAMBDA --> SNS["SNS: notifications"]:::orange
Classic CDC pattern inside AWS: changes in DynamoDB propagate to other systems via Streams + Lambda.
S3 vs EBS vs EFS
| S3 | EBS | EFS | |
|---|---|---|---|
| Type | Object storage | Block storage | Network filesystem (NFS) |
| Access | HTTP API, SDK | Mounted on one EC2 (Multi-Attach: up to 16) | Mounted on many EC2 simultaneously |
| Durability | 11 nines (3+ AZ) | Single AZ (snapshot to S3 for backup) | Multi-AZ |
| Max size | Unlimited | 64 TB per volume | Unlimited |
| Latency | ms-level | Sub-ms | Low ms |
| Use case | Static files, backups, data lake, artifacts | OS volumes, DB storage | Shared config, CMS, home dirs |
| Cost | $0.023/GB | $0.10/GB (gp3) | $0.30/GB |