Software · Skills roadmap
Software Engineer (Backend) interview questions
Build the services, APIs and data that products run on, and keep them fast and reliable.
- 36 to 60 weeks to job-ready, part time
- 69 lessons across 21 skills
- 12 portfolio projects
Try each question out loud before opening the answer. Shared questions (tagged by skill) also appear on other roles that use the same skill.
Concepts
What do the status codes 200, 201, 202, 400, 401, 403, 404, 409, 429 and 503 mean?
200 OK, success with a body. 201 Created, a new resource was created. 202 Accepted, the request was received and will be processed later (common for webhooks and long jobs). 400 Bad Request, the input is invalid. 401 Unauthorized, missing or invalid credentials. 403 Forbidden, authenticated but not allowed. 404 Not Found. 409 Conflict, for example a duplicate or a version conflict. 429 Too Many Requests, you are rate limited. 503 Service Unavailable, a temporary server problem, often worth retrying later.
What is the test pyramid, and how would you apply it to a backend service?
Many fast unit tests at the bottom, fewer integration tests in the middle, and a small number of end-to-end tests on top. For a service: unit tests for business rules and edge cases; integration tests that call endpoints with a real database (for example in a container) to check SQL, serialisation and status codes; and a few end-to-end tests of the critical user journeys in a deployed environment. Lower levels are faster and more precise about what broke; higher levels catch wiring problems.
What is the difference between WHERE and HAVING?
WHEREfilters rows before grouping;HAVINGfilters groups after aggregation. SoWHERE fare_amount > 0removes refunds before you count, whileHAVING count(*) > 100000keeps only busy zones after counting. Aggregates such ascount(*)cannot appear inWHERE. Filtering inWHEREwhen you can is also faster, because fewer rows reach the grouping step.When can a LEFT JOIN return more rows than the left table has?
When the right table has more than one match for a key. Each left row is repeated once per match. Check the key is unique on the right side (
count(*)versuscount(distinct key)) and compare row counts before and after the join. Duplicating a single zone in a lookup table, for example, inflated a 3.48 million row taxi join by about 170,000 rows.What is the contract between equals and hashCode in Java, and what breaks if you violate it?
If two objects are equal according to
equals, they must return the samehashCode. Hash-based collections such asHashMapandHashSetfirst use the hash code to find a bucket, thenequalsto compare within it. If you overrideequalsbut nothashCode, two equal objects usually land in different buckets, so aHashSetcan contain duplicates andmap.get(key)can miss an entry you just put in. Records generate both methods consistently from their fields, which is one reason to use them for value objects.What is the difference between checked and unchecked exceptions, and when would you use each?
Checked exceptions (subclasses of
Exceptionbut notRuntimeException) must be declared or caught, so the compiler forces callers to deal with them; they suit recoverable conditions the caller can reasonably handle, such as a missing file. Unchecked exceptions (RuntimeExceptionand subclasses) signal programming errors or conditions most callers cannot handle, such as invalid arguments or illegal state. Many modern codebases prefer unchecked exceptions with a global handler at the boundary, and use checked ones sparingly.Why prefer immutable objects, and how do records help?
Immutable objects cannot change after construction, so they are safe to share between threads without locks, safe to use as map keys, and easier to reason about because no other code can modify them behind your back. A record declares its fields once and gets a canonical constructor, accessors,
equals,hashCodeandtoString, with final fields. Note that a record holding a mutable list is only shallowly immutable; copy it withList.copyOfin the constructor.What is the difference between a mock, a stub and a fake?
A stub returns canned answers to calls, so the code under test can run. A mock also records how it was called, so the test can verify interactions (called once with these arguments). A fake is a working but simplified implementation, such as an in-memory repository. Use them for slow or external boundaries such as networks, clocks and payment providers, and prefer real collaborators for your own simple code, since over-mocking makes tests pass while the real integration is broken.
What is idempotency, and why does it matter when calling or building APIs?
An operation is idempotent if doing it several times has the same effect as doing it once. Networks fail after a request is processed but before the response arrives, so clients retry, and non-idempotent operations such as "create payment" can then happen twice. GET, PUT and DELETE are idempotent by design; for POST, use an idempotency key: the client sends a unique key per logical operation and the server stores the result for that key, returning it again for repeats instead of acting twice.
How do you secure a webhook endpoint?
Verify every request: most providers sign the raw body with an HMAC and a shared secret, so recompute the signature over the exact bytes received and compare in constant time. Reject requests with old timestamps to limit replays, and store processed event ids so duplicates are ignored. Use HTTPS, limit the body size, validate the payload, and return 2xx quickly before doing slow work in the background. Keep the secret in a secrets manager and support rotating it.
Explain ROW_NUMBER, RANK and DENSE_RANK.
For values 10, 9, 9, 7 ordered descending:
row_numbergives 1, 2, 3, 4 (always unique);rankgives 1, 2, 2, 4 (ties share, then a gap);dense_rankgives 1, 2, 2, 3 (ties share, no gap). Userow_numberfor "exactly one row per group",dense_rankfor "Nth distinct value".Which failed API requests should you retry, and how?
Retry transient failures: network errors, timeouts, 429 (rate limited) and most 5xx responses. Do not retry 4xx client errors such as 400, 401, 403 or 404, because they will fail again. Use exponential backoff with jitter and a maximum number of attempts, and respect a
Retry-Afterheader when present. Only retry operations that are idempotent or protected by an idempotency key. Log each attempt with a request id, and send items that still fail to a dead-letter store for review.How does a HashMap work internally?
It keeps an array of buckets. A key's
hashCodeis spread and mapped to a bucket index; entries in the same bucket are kept in a linked list, which Java converts to a balanced tree when a bucket grows large (more than 8 entries in a big enough table). When the number of entries passes capacity times the load factor (0.75 by default), the table doubles and entries are redistributed. Lookups are O(1) on average and degrade when many keys collide.HashMapis not thread-safe; useConcurrentHashMapfor shared access.Two threads increment a shared counter a million times each and the result is less than two million. Why, and how do you fix it?
count++is a read, an add and a write. Two threads can read the same value and both write back the same result, losing an update: a race condition. Fix it by making the update atomic:AtomicInteger.incrementAndGet(), asynchronizedblock, or aLongAdderfor heavy contention.volatilealone is not enough, because it guarantees visibility, not atomicity. Better still, avoid shared mutable state, for example by having each thread count locally and combining at the end.What is dependency injection in Spring, and why prefer constructor injection?
Spring creates and manages objects (beans) and supplies each one's dependencies instead of the object creating them itself. With constructor injection, dependencies are parameters of the constructor, so they are explicit, can be
final, the object is always fully initialised, and tests can pass in fakes without Spring. Field injection with@Autowiredhides dependencies, allows partly built objects and makes plain unit tests harder.When would you choose a document database over a relational one?
When the data is naturally a self-contained document read and written as a whole (a product with varied attributes, a user profile, an event), the schema varies between records, and the access patterns are known and mostly by key. Relational databases fit better when data has many relationships queried in many directions, when you need multi-row transactions and strong constraints, and when ad hoc reporting matters. Many teams use both, and PostgreSQL's JSON columns cover a lot of "flexible schema" needs.
Why does the order of fields in a compound index matter?
A compound index is sorted by its first field, then the second within it, and so on, like a phone book sorted by surname then first name. A query can use the index efficiently only for a prefix of its fields. For a query that filters on equality, sorts, and filters on a range, the common guideline is equality fields first, then sort fields, then range fields, so the database can walk the index in order without a separate sort. Check with the query plan.
When should services call each other synchronously, and when should they communicate with events?
Use a synchronous call when the caller needs the answer to continue, for example checking stock before confirming an order, and keep the chain short with timeouts. Use events when other services need to react but the caller does not need to wait: sending a confirmation email, updating loyalty points, analytics. Events decouple services and absorb spikes, at the cost of eventual consistency, duplicate handling and harder debugging. A common design is synchronous for the critical path and events for everything that follows.
You add read replicas to a database. What new problem appears, and how do you handle it?
Replication lag: replicas can be slightly behind the primary, so a user who just changed something may read the old value from a replica. Handle it by reading the user's own recent writes from the primary (for example for a few seconds after a write, or for specific pages), using session tokens that carry the latest write position, or accepting staleness where it does not matter. Replicas add read capacity only; writes still go to one primary.
How should errors be handled and wrapped in Go?
Return errors as the last value and check them immediately. Add context when passing an error up with
fmt.Errorf("loading user %d: %w", id, err), which keeps the original error for inspection. Callers test for a specific cause witherrors.Is(sentinel errors) or extract a typed error witherrors.As. Do not both log and return the same error, which duplicates logs, and avoidpanicfor expected failures.What is the difference between liveness and readiness probes, and how would you set them for a model server?
A liveness probe answers "is the process stuck?"; if it fails, Kubernetes restarts the container. A readiness probe answers "can this pod take traffic now?"; if it fails, the pod is removed from the Service endpoints without a restart. For a model server, make readiness pass only after the model has loaded, and keep liveness cheap and independent of downstream services, so slow model loading or a busy dependency does not cause restart loops. Use a startup probe or an initial delay when loading takes a long time.
A dashboard query takes two minutes. How do you speed it up?
Read the plan with
EXPLAIN ANALYZE. Look for full scans of large tables, non-sargable filters (functions on indexed columns), joins on unindexed keys and row estimates far from reality. Fixes: filter early, select only needed columns, add or fix indexes, rewrite date filters as ranges, pre-aggregate into a summary table, and on warehouses use partitioning and clustering to cut bytes scanned. Measure before and after.How do partitions and consumer groups work in Kafka?
A topic is split into partitions; order is guaranteed only within a partition, and events with the same key always go to the same partition. Consumers in a group share partitions, each partition read by one member at a time, so parallelism is capped by the partition count. Each group commits offsets per partition to remember its position.
How do you avoid duplicates when a stream consumer crashes and restarts?
Assume at-least-once delivery: process, then commit offsets. Make the sink idempotent with a unique event id and an upsert, or record processed ids in the same transaction as the result. Enable producer idempotence to avoid duplicates at the source, route poison messages to a dead-letter topic, and remember that side effects (emails, dead letters) also repeat on replay.
What does @Transactional do, and what are common pitfalls?
It wraps the method in a database transaction through a proxy: commit on normal return, roll back on unchecked exceptions (checked exceptions do not roll back by default). Pitfalls: calling a
@Transactionalmethod from another method in the same class bypasses the proxy, so no transaction starts; long transactions that include remote calls hold locks and connections; and assuming it protects against lost updates without the right isolation level or optimistic locking (@Version). Keep transactions short and around the database work only.What is consistent hashing and why is it used?
It places both servers and keys on a hash ring and assigns each key to the next server clockwise. When a server is added or removed, only the keys in its segment move, roughly one in n, instead of almost all keys as with
hash(key) % n. Virtual nodes (many points per server) spread load evenly. It is used for sharding databases and distributing cache keys, for example in Cassandra and DynamoDB-style systems and in many client-side cache libraries.
Coding
Given a list of order amounts and a target, return whether any two different orders add up to the target. What is your approach and its complexity?
The brute-force approach checks every pair: O(n squared) time, O(1) extra space. Better: walk the list once, and for each amount check whether
target - amountis in a hash set of amounts seen so far, then add the current amount: O(n) time on average and O(n) space. If the list is already sorted, two pointers from both ends give O(n) time with O(1) space. State the trade-off and test edge cases: empty list, one element, duplicates and negative values.Your model-serving Docker image is 6 GB. How would you make it smaller?
Find what is taking the space with
docker historyor a tool such as dive. Then use a slim base image, a multi-stage build so compilers and build tools stay out of the final image, install only runtime dependencies (for example CPU-only framework wheels if you serve on CPU), clear package caches in the same layer, add a.dockerignoreso data and notebooks are not copied, and keep large model files out of the image when they can be pulled from a registry or volume at start-up. Order layers so dependencies are cached before the application code.What is a goroutine leak, and how do you prevent one?
A goroutine that never finishes, usually because it waits forever on a channel nobody will send to or receive from, or on I/O with no timeout. Leaked goroutines hold memory and resources until the process restarts. Prevent it by giving every goroutine a way to stop: pass a
context.Contextand select onctx.Done(), close channels from the sender when done, use buffered channels where a result may be abandoned, and put timeouts on network calls.goleakand the runtime's goroutine count help detect leaks in tests.
Case studies
A test fails about one run in twenty in CI. How do you deal with it?
Treat it as a bug, not bad luck. Reproduce it by running the test many times, in random order and in parallel. Common causes: shared state between tests (a database, static variables, files), dependence on time or timezone, unordered collections, real network calls, and sleeps used instead of waiting for a condition. Fix the cause: isolate state per test, inject a clock, wait on conditions with timeouts, and mock external calls. Quarantining without a fix only hides a real defect or a real race in production code.
An endpoint that lists 50 orders with their items runs 51 SQL queries. What is happening and how do you fix it?
That is the N+1 query problem: one query loads the orders, then lazy loading fires one query per order for its items. Fix it by fetching what the endpoint needs in one or two queries: a fetch join or entity graph in JPA, a batch fetch size, or a query that returns a DTO directly. Confirm with SQL logging or a test that asserts the number of queries, and watch pagination, because fetch joins on collections can break database-side paging.
You add a Redis cache for product pages. Prices sometimes show old values for minutes. How do you fix it?
Delete (or update) the cached key whenever the price changes, in the same code path that writes to the database, and keep a TTL as a safety net. Be careful about ordering: write the database first, then delete the cache key, so a concurrent reader does not re-cache the old value; for strict cases, delete again shortly after or version the keys. If updates come from other systems, publish change events and invalidate from a consumer. Measure hit rate and staleness, and use shorter TTLs for data where staleness costs money.
A downstream service becomes slow and your whole site starts timing out. What happened and how do you prevent it?
Requests to the slow service held threads and connections while waiting, so the calling service ran out of them and started failing too, and the failure spread upstream: a cascading failure. Prevent it with timeouts on every call, a circuit breaker that fails fast when the dependency is unhealthy, bulkheads (separate pools per dependency), limited retries with backoff and jitter, and a fallback. Watch saturation metrics such as pool usage and queue length, not just error rates.
Placing an order must reserve stock, charge a payment and create a shipment, each in a different service. How do you keep them consistent?
Avoid distributed two-phase commit. Use a saga: a sequence of local transactions, each publishing an event (or driven by an orchestrator), with a compensating action for each step, such as releasing the stock if payment fails. Publish events reliably with an outbox, make every step idempotent, and model intermediate states explicitly (payment pending) so users see something truthful while the saga runs. Monitor stuck sagas and have a way to retry or fix them.
System design
Design a URL shortener.
Clarify requirements (create, redirect, expiry, custom codes, analytics?) and estimate: for example 100 million new links a month is about 39 writes per second and, at 100 reads per write, about 3,900 redirects per second, with about 3 TB over five years at 500 bytes per link. Design: stateless servers behind a load balancer; a key-value table keyed by code; a cache for popular links. Deep dives: code generation (random base-62 with a uniqueness check, or an encoded counter), 301 versus 302, abuse prevention and what happens when the cache or database fails.
Design a rate limiter for a public API.
Clarify: limits per user, per API key or per IP; hard or soft; global or per region. Algorithms: fixed window (simple, bursty at edges), sliding window log or counter (smoother), token bucket (allows bursts up to a size, common in practice). Store counters in Redis with atomic operations or a Lua script so all API servers share state; return 429 with
Retry-After. Discuss failure: if Redis is down, fail open or closed per endpoint; and latency: keep the check to one round trip, or use local approximations synchronised periodically.
Behavioural
How do you explain a technical finding to a non-technical audience?
Lead with the decision or finding, use one number with context, and replace method with an analogy ("we scored customers on how recently, how often and how much they buy"). Show one clear chart titled with the takeaway. Keep methods in an appendix and check understanding by asking what they would do next.
Tell me about a time a stakeholder disagreed with your analysis.
Use situation, task, action, result. Show that you first understood their view (often a different definition or context), checked your own work, found the source of the difference together and agreed on the next step. End with the outcome and what you changed in how you work, for example writing definitions into every brief.
Tell me about a production issue you handled.
Use a STAR story. Show how you detected it (alert, user report), how you limited the damage first (rollback, feature flag, scaling), how you found the root cause with evidence (logs, metrics, traces), how you communicated during the incident, and what changed afterwards (a test, an alert, a runbook, a design fix) as part of a blameless review. Interviewers look for calm prioritisation and learning, not heroics.
Three teams need analysis from you this week. How do you prioritise?
Ask each team what decision depends on the work and by when. Rank by impact and deadline, share the proposed order with the requesters and your manager, and make trade-offs explicit ("I can do A and B this week, C next Tuesday"). Offer smaller versions where a rough answer is enough. Agree it in writing so nobody is surprised.