Backend Engineer interview prep
A spaced-repetition deck of 625+ Backend Engineer interview questions — organised by topic and difficulty, and resurfaced right before you'd forget. Preview a few cards below, then choose access to study the whole track on an Anki-style SM-2 schedule.
7 days free on monthly or yearly · every feature included.
What's covered
Every topic in this track, grouped the way you'd study it.
Distributed Systems
12 cardsKafka & Messaging
12 cardsCaching Deep-Dive
10 cardsPerformance & Concurrency
11 cardsReliability & Incidents
10 cardsPython
122 cardsDatabases
104 cardsBackend
169 cardsCS Fundamentals
76 cardsDevOps & Infra
35 cardsSystem Design
29 cardsBehavioral
35 cardsSample questions
A few cards from the deck — reveal each answer, then choose access to study the full set on a schedule.
Event sourcing and CQRS: when are they justified and what is the price?
Event sourcing and CQRS: when are they justified and what is the price?
Short answer: Event sourcing: state is not stored but derived — state = fold(events); the event log is primary. CQRS: the write model and the read models are separated. You get a full audit trail, temporal queries ('what did the order look like yesterday'), and rebuildable projections. The price is steep: event versioning, snapshots, eventually consistent read models, expensive tooling. It is not a default architecture.
In depth:
events: OrderCreated → ItemAdded → ItemAdded → OrderPaid
state = fold(events) — always derivable from scratch
projections: 'orders per day', 'top items' — separate read
models, rebuildable from the log at any time
- When justified — money movements, ledgers, audit-heavy and compliance-bound domains; 'why did the balance end up like this' is a business question, not a log-digging exercise.
- Price #1: versioning — an event lives forever; when the schema changes you must read every old version (upcasters) or migrate the whole log.
- Price #2: reads — read models are async: right after a command the projection lags, and the UI, tests, and support must live with that.
- CQRS without ES — legitimate and far cheaper: a regular write DB + denormalized read projections.
⚠️ Common mistake: proposing event sourcing for a CRUD app 'for future growth'. If audit is not a business requirement, you pay the full ES price and gain nothing a table plus a change log would not give you.
How does a consumer group get partitions assigned, and what happens when there are more consumers than partitions?
How does a consumer group get partitions assigned, and what happens when there are more consumers than partitions?
Short answer: Within a group, each partition is read by at most one consumer. More consumers than partitions — the extras sit idle: a group's maximum parallelism equals the partition count. Different groups read the same topic independently, each with its own offsets.
In depth:
topic orders: p0 p1 p2 p3
group A (3 consumers):
c1 ◄─ p0, p1 c2 ◄─ p2 c3 ◄─ p3
group A (6 consumers):
c1◄p0 c2◄p1 c3◄p2 c4◄p3 c5, c6 — idle
group B: reads the same partitions independently (own offsets)
- Within a group — a partition goes to exactly one consumer: this preserves processing order within the partition.
- Across groups — independent reads: each group commits its own offsets to
__consumer_offsets, so the same data serves both billing and analytics. - Planning — the partition count caps how far the group can scale; provision it with headroom when creating the topic.
⚠️ Common mistake: 'add more consumers and it gets faster.' Beyond the partition count, added consumers simply sit idle.
Cache-aside vs write-through vs write-behind: how does each work and when do you choose it?
Cache-aside vs write-through vs write-behind: how does each work and when do you choose it?
Short answer: Cache-aside — the app reads the cache, on a miss goes to the DB and stores the result; on writes it updates the DB and invalidates the key. Write-through — writes go to cache and store synchronously. Write-behind — the cache acks immediately and flushes to the store asynchronously. The default is cache-aside; the other two fit specific workload profiles.
In depth:
| Cache-aside | Write-through | Write-behind | |
|---|---|---|---|
| Read | miss → DB → cache | from cache | from cache |
| Write | DB + invalidate the key | cache + DB synchronously | cache now, DB later |
| Consistency | stale window after a write | readers see fresh data | weak until the flush |
| Write latency | same as the DB | DB + cache: slower | cache only: fast |
| Loss risk | none | none | yes: crash before flush |
- Cache-aside — the cache is off the write critical path and its outage is survivable (just more misses); the price — your own invalidation protocol on every write.
- Write-through — consistent reads out of the box; the price — every write waits on both stores, and you cache things nobody will ever read.
- Write-behind — a buffer for write bursts (counters, likes, metrics); without a durable buffer (a queue, AOF) a node crash = lost writes.
⚠️ Common mistake: picking write-behind 'for speed' without answering what happens to unflushed writes when the process dies.
N+1 queries: how do you notice them and how do you fix them?
N+1 queries: how do you notice them and how do you fix them?
Short answer: N+1 is one query for the list plus one more query per element. The symptom: latency grows linearly with collection size, and the log shows a run of identically shaped queries differing only in the id. The cure is eager loading or batching by ids.
In depth:
- Notice it — the SQL log (echo, debug toolbar): dozens of identical
SELECT … WHERE id = ?; in tests — assert the query count (django_assert_num_queries, a counter on SQLAlchemy events). - Fix it — eager loading: select_related/prefetch_related in Django, joinedload/selectinload in SQLAlchemy; or collect the ids by hand and issue a single
WHERE id IN (…). - Pin it down — with that same query-count test: a regression can no longer slip into prod silently.
# before: 1 + N queries
for order in orders:
print(order.customer.name) # a query on every iteration
# after: a JOIN, one query
orders = Order.objects.select_related("customer")
⚠️ Common mistake: prefetching, then calling .filter()/.count() on the related collection inside a loop — the ORM goes back to the DB on every iteration and the N+1 returns, even though "we do have a prefetch".
A provider timeout happened mid-payment: was the money charged or not? What does your code do?
A provider timeout happened mid-payment: was the money charged or not? What does your code do?
Short answer: Unknown — a timeout means 'the response never arrived', not 'the operation never happened'. Blindly retrying a non-idempotent charge is the straight road to a double charge. The right move: query the provider's status API / wait for the webhook, and final consistency comes from scheduled reconciliation against the provider's reports.
In depth:
t0 our service ──► POST /charge ──► provider
t1 provider charges the money ✓
t2 the response is lost in the network ✗
t3 we time out: outcome UNKNOWN (window t1–t3)
- Three indistinguishable outcomes — the request never arrived; the provider crashed mid-operation; the response was lost on the way back. From the outside they look identical.
- No blind retry — our payment stays PENDING; we poll the provider's status API or wait for the webhook, and only then advance the state machine.
- Provider-side idempotency — pass our idempotency key to the provider: then even a retry is safe by design, the provider rejects the duplicate itself.
- Reconciliation — a scheduled job compares our stuck/final statuses against the provider's reports and closes the gaps: money never goes missing silently.
⚠️ Common mistake: treating the timeout as 'not charged' and marking the payment FAILED — the customer pays a second time, and the first charge surfaces later during reconciliation.
What's the difference between mutable and immutable types?
What's the difference between mutable and immutable types?
Short answer: Mutable objects can be changed in place without creating a new object (list, dict, set, bytearray); immutable ones cannot (int, float, str, bytes, tuple, frozenset, bool, None). Any "change" to an immutable object creates a new object.
In depth: Mutability is about whether an object can change its contents while keeping the same id() (its address in memory).
# Immutable: the operation creates a NEW object
s = "hello"
print(id(s))
s += " world" # a new string is created
print(id(s)) # id changed — it's a different object
# Mutable: the same object is changed
lst = [1, 2, 3]
print(id(lst))
lst.append(4) # change in place
print(id(lst)) # same id
Why it matters:
- Hashability. Only objects that are immutable (by content) can be used as
dictkeys orsetelements. If a key could change, its hash would drift and it couldn't be found. - Safety when sharing. An immutable object can be safely shared across threads/functions — no one can corrupt it.
- Function arguments. Python passes a reference to the same object: a function can mutate a mutable argument's contents, but rebinding the local parameter does not rebind the caller's variable.
⚠️ Gotcha: A tuple is immutable, but if it holds a list, that list can still be changed:
t = ([1, 2], 3)
t[0].append(99) # OK! the list inside is mutable
print(t) # ([1, 2, 99], 3)
# t[0] = [...] # THIS is a TypeError — you can't reassign an element
Also, hash(([1,2], 3)) will fail — a tuple is unhashable if it contains an unhashable element.
Ready to make it stick?
Start your first session in under a minute. Your future self, mid-interview, will thank you.
Questions about this track
How should I prepare for a Backend Engineer interview?
Study the concepts you'll be asked to explain, not just the ones you can code. RecallDeck's Backend Engineer track gives you 625+ curated interview questions and resurfaces each one with an Anki-style SM-2 schedule right before you'd forget it — so the answers are still there under pressure on interview day.
What topics does the Backend Engineer track cover?
The Backend Engineer track is organised into the core areas Backend Engineer interviews actually test, grouped by topic and by difficulty (Concept, Junior, Middle, Senior). You can preview the full outline and sample questions above before signing in.
Is spaced repetition effective for Backend Engineer interview prep?
Yes. Actively recalling an answer and grading yourself honestly builds far more durable memory than re-reading notes. RecallDeck schedules each Backend Engineer card to reappear at the moment you're about to forget it, so your daily reviews shrink while your recall holds.
Can I try the Backend Engineer track before paying?
Yes. Monthly and yearly access include a seven-day trial of the complete Backend Engineer track, the full SM-2 scheduler, statistics, flexible pacing, and cram mode. You can cancel online before the first charge.