Free lesson · AWS for System Design Interviews
The service-to-primitive map
How To Use This
A working reference for translating in both directions: from a requirement to a primitive to a service, and from a service back to the neutral vocabulary that works in any interview.
Organised by primitive, because that is the direction reasoning actually runs. You do not start with a service and look for a problem.
Tip: The column that matters most in an interview is the third one - what the primitive guarantees.
Naming the primitive is the entry price. Stating what it guarantees is the signal, because it demonstrates the choice was made against requirements rather than by association.
"I need an append-only log - ordered within a partition, retained so consumers can replay, each consumer holding its own offset" is a complete design statement. The service name that follows is almost incidental, which is the point.
Storage Primitives
| Need | Primitive | Guarantees | AWS | Elsewhere |
|---|---|---|---|---|
| Store blobs, retrieve whole | Object store | Durable, HTTP, flat namespace, not a filesystem | S3 | GCS, Blob Storage, MinIO |
| A disk for one machine | Block storage | Low latency, single-attach, AZ-scoped | EBS | Persistent Disk, Managed Disks |
| Shared filesystem | Distributed filesystem | POSIX, multi-attach, higher latency | EFS, FSx | Filestore, Azure Files, NFS |
| Fast scratch | Local disk | Fastest, ephemeral | Instance store | Local SSD |
| Cheap long-term retention | Archive storage | Cheap storage, slow and costly retrieval | Glacier tiers | Coldline, Archive |
Data Primitives
| Need | Primitive | Guarantees | AWS | Elsewhere |
|---|---|---|---|---|
| Lookup by key, huge scale | Partitioned key-value store | Predictable latency, hot-key ceiling, fixed access patterns | DynamoDB | Cassandra, Bigtable, Cosmos DB |
| Joins, transactions, ad-hoc queries | Relational database | ACID, flexible querying, single writer | RDS, Aurora | PostgreSQL, MySQL, Cloud SQL |
| Relational at write scale | Distributed SQL | ACID across partitions, higher latency | Aurora Limitless, DSQL | Spanner, CockroachDB, Vitess |
| Sub-millisecond reads | In-memory cache | Fast, volatile, limited by memory | ElastiCache, MemoryDB | Redis, Memcached |
| Full-text and faceted search | Search index | Inverted index, eventually consistent with the source | OpenSearch | Elasticsearch, Solr |
| Time-ordered measurements | Time-series store | Optimised for time-range queries, downsampling | Timestream | Prometheus, InfluxDB |
| Relationships and traversal | Graph database | Efficient multi-hop traversal | Neptune | Neo4j, JanusGraph |
| Analytical scans | Columnar warehouse | Column pruning, compression, not for point reads | Redshift, Athena | BigQuery, Snowflake |
| Similarity search | Vector index | Approximate nearest neighbour | OpenSearch, Aurora pgvector | Pinecone, FAISS |
Warning: The two rows most often confused are the partitioned key-value store and the relational database, and the discriminator is not scale.
It is whether the access patterns are known and bounded. A key-value store fixes them at design time via the key; adding a new one later means an index or a migration. A relational database lets you ask questions you did not anticipate.
For a system whose queries are still being discovered - most new products - relational is the honest choice, and "it will not scale" is usually premature. A well-indexed relational database handles far more than candidates assume.
Messaging Primitives
| Need | Primitive | Guarantees | AWS | Elsewhere |
|---|---|---|---|---|
| Distribute work, absorb bursts | Queue | Consumed once, at-least-once, no ordering unless FIFO | SQS | RabbitMQ, Cloud Tasks |
| Ordered, replayable event stream | Append-only log | Ordered per partition, retained, per-consumer offsets | Kinesis, MSK | Kafka, Pub/Sub |
| One event, many independent consumers | Pub/sub | Fanout, no retention, at-least-once | SNS, EventBridge | Pub/Sub, NATS |
| Multi-step process with state | Workflow engine | Durable execution state, retries, compensation | Step Functions | Temporal, Cadence, Airflow |
| Scheduled work | Scheduler | Time-based triggering | EventBridge Scheduler | cron, Cloud Scheduler |
Compute Primitives
| Need | Primitive | Guarantees | AWS | Elsewhere |
|---|---|---|---|---|
| Run a process per event | Function runtime | Instant scale, duration and size caps, stateless | Lambda | Cloud Functions, Knative |
| Run containers without hosts | Managed container runtime | No host management, scale-out lag | Fargate | Cloud Run, Container Apps |
| Orchestrate containers | Container orchestrator | Desired-state reconciliation | ECS, EKS | Kubernetes, Nomad |
| Full control of the machine | Virtual machine | Everything, and you operate it | EC2 | Compute Engine, VMs |
| Bounded parallel jobs | Batch scheduler | Queueing, retries, interruptible capacity | AWS Batch | Slurm, Kubernetes Jobs |
Edge, Network, And Coordination
| Need | Primitive | Guarantees | AWS | Elsewhere |
|---|---|---|---|---|
| Cache near users | CDN | Edge caching, invalidation is the hard part | CloudFront | Cloudflare, Fastly, Akamai |
| Distribute across servers | Load balancer | Health checking, L7 routing or L4 forwarding | ALB, NLB | nginx, Envoy, HAProxy |
| Name resolution and failover | DNS | Caching you do not control | Route 53 | any DNS provider |
| API front door | Reverse proxy / gateway | Auth, throttling, transformation | API Gateway | Kong, Envoy, nginx |
| Distributed locks, leader election | Coordination service | Consensus, the hardest thing here | DynamoDB conditional writes | ZooKeeper, etcd, Consul |
| Store credentials | Secrets store | Encrypted, access-controlled, rotatable | Secrets Manager | Vault, Secret Manager |
| Manage encryption keys | Key management | Hardware-backed hierarchy, envelope encryption | KMS | Cloud KMS, Vault |
Important: The coordination row deserves attention because it is the one candidates most often wave at.
"I'd use a distributed lock" is a sentence that hides the hardest problem in distributed systems. Consensus is expensive, lock holders fail while holding locks, and lease expiry versus work completion is a genuine correctness problem - a holder can be paused past its lease and resume believing it still holds the lock.
On AWS the pragmatic answer is usually a conditional write to DynamoDB with a lease and a fencing token, not a general lock service. The stronger answer is usually to avoid needing the lock - partition the work so each partition has a single natural owner, or make the operation idempotent so concurrent execution is safe.
An interviewer who hears "I'd avoid needing a distributed lock by partitioning" has learned more than from any lock implementation.
Choosing Between Two Services For One Primitive
The map gets you to a primitive. Frequently two services implement it, and the discriminator is rarely the primitive itself.
| Both provide | Discriminator |
|---|---|
| SQS vs Kinesis | Queue or log - will anything replay? |
| ECS vs EKS | Operational cost - can you staff Kubernetes? |
| RDS vs Aurora | Read scaling and failover speed vs cost |
| ElastiCache vs MemoryDB | Durability - is cache loss acceptable? |
| SNS vs EventBridge | Routing sophistication vs simplicity |
| Lambda vs Fargate | Demand shape and duration |
| DynamoDB vs Cassandra | Managed vs control, and where you run |
The discriminator is usually operational cost, durability, or demand shape - not capability. Two services implementing the same primitive largely do the same thing; what differs is what they demand of you and what they guarantee at the edges.
In an interview, naming the discriminator is the signal. "Either works - I'd choose ECS because we cannot staff two or three Kubernetes engineers, and that is the actual constraint" demonstrates you evaluated it. "I'd use EKS" does not.
Translating In Both Directions
graph LR
R[**Requirement**<br/>'users must see<br/>their own posts<br/>immediately'] --> P[**Property**<br/>read-your-writes,<br/>bounded by user]
P --> PR[**Primitive**<br/>key-value store,<br/>partitioned by user]
PR --> S[**Service**<br/>DynamoDB,<br/>or Cassandra]
S -.->|**Reverse direction**:<br/>interviewer names<br/>a service| PR
PR -.->|state the primitive<br/>and its guarantees| P
This diagram shows translation running in both directions. A requirement, such as users must see their own posts immediately, produces a property - read-your-writes bounded by user. That identifies a primitive, a key-value store partitioned by user. That identifies a service, DynamoDB or Cassandra. In the reverse direction, when an interviewer names a service, translate back to the primitive and state its guarantees rather than accepting the product name as the unit of discussion.
The reverse direction matters more than people expect. When an interviewer says "suppose we use Kafka here", the strong response translates: "so an append-only log - ordered per partition, retained, consumers holding offsets. That means I can add the analytics consumer later without touching the ingestion path."
You have accepted their framing and demonstrated you know what it buys.
What The Map Does Not Contain
Operational cost, which is frequently the deciding factor. Two services implementing the same primitive can differ enormously in what they demand of a team - EKS and ECS are the standing example.
Maturity and ecosystem. The primitive is identical; the tooling around it is not.
The specific limits. Deliberately absent, because they change. Reason about the shape and verify the number - Module 02 Topic 4.
Common Failure Modes
Warning: Memorising the map without the guarantees column. The signal is in the guarantees.
Queue chosen where a log was needed. Not reversible cheaply.
Key-value store chosen for unknown access patterns.
"It will not scale" applied to relational databases prematurely.
Search index treated as a source of truth. Eventually consistent with it.
Distributed locks waved at. The hardest thing in the table.
Columnar warehouse used for point reads.
Only translating one direction. The reverse is common in interviews.
Operational cost ignored. Same primitive, very different burden.
Specific limits quoted from memory.
Why This Matters In An Interview
Interview: What interviewers are listening for:
- The primitive named with its guarantees, before any product.
- Translating a service the interviewer named back into neutral terms.
- The queue/log discrimination.
- Avoiding coordination rather than implementing it.
Common follow-ups:
- "What would you use for the event pipeline?" → An append-only log: ordered per partition, retained so consumers replay, per-consumer offsets. That lets analytics be added later without touching ingestion. Kinesis or Kafka.
- "Suppose we already run Kafka." → Then it is an append-only log with the same properties. The design does not change; ordering is per partition, so the partition key must carry whatever ordering the domain requires.
- "How do you ensure only one worker processes each job?" → Preferably by partitioning so each key has one natural owner, or by making processing idempotent. If a lock is genuinely required, a conditional write with a lease and a fencing token - and the lease-versus-completion problem is real.
- "Would a relational database work here?" → Frequently yes, and further than expected. It depends on whether the access patterns are known and bounded. If they are still being discovered, relational is the honest choice.
Practice
Exercise: Starter. Cover the AWS column and, for each primitive, name the service. Then cover the primitive column and do the reverse.
Intermediate. For every service in a design you can already give, write the primitive and its guarantees. Deliver the design using only that vocabulary.
Advanced. Take a design that uses a distributed lock and redesign it so no lock is needed - by partitioning, or by making the operation idempotent.
Related Topics
- Topic 2 - Why each service maps to its primitive
- Topic 4 - When to use which vocabulary
- Module 02 Topic 4 - The limits deliberately absent here
- Module 03 - The map applied across eight designs