RecallDeck
Interview track

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.

625 cards22 topics
See plans and start trial

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 cards
Distributed Systems

Kafka & Messaging

12 cards
Kafka & Messaging

Caching Deep-Dive

10 cards
Caching

Performance & Concurrency

11 cards
Performance

Reliability & Incidents

10 cards
Reliability & Incidents

Python

122 cards
Core LanguageData Model & InternalsConcurrency & AsyncStdlib, Typing & Testing

Databases

104 cards
SQL FundamentalsPostgres Internals & OptimizationNoSQL & Redis

Backend

169 cards
HTTP & REST APIArchitecture & ScalingAuth & SecurityDjango & FastAPIORM & SQLAlchemy

CS Fundamentals

76 cards
Data Structures & AlgorithmsNetworks

DevOps & Infra

35 cards
Docker, CI/CD & Linux

System Design

29 cards
System Design

Behavioral

35 cards
Behavioral

Sample 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?

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
  1. 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.
  2. Price #1: versioning — an event lives forever; when the schema changes you must read every old version (upcasters) or migrate the whole log.
  3. Price #2: reads — read models are async: right after a command the projection lags, and the UI, tests, and support must live with that.
  4. 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?

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)
  1. Within a group — a partition goes to exactly one consumer: this preserves processing order within the partition.
  2. Across groups — independent reads: each group commits its own offsets to __consumer_offsets, so the same data serves both billing and analytics.
  3. 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?

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
  1. 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.
  2. Write-through — consistent reads out of the box; the price — every write waits on both stores, and you cache things nobody will ever read.
  3. 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?

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:

  1. 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).
  2. 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 (…).
  3. 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?

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)
  1. 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.
  2. 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.
  3. Provider-side idempotency — pass our idempotency key to the provider: then even a retry is safe by design, the provider rejects the duplicate itself.
  4. 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?

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 dict keys or set elements. 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.

Other interview tracks

RecallDeckSpaced-repetition interview prep