Senior Software Engineer interview questions
Reviewed by Mark Dickie · Last updated
Senior Software Engineer interviews are assessments of production-level engineering depth across databases, backend code, distributed system design, and cloud infrastructure. The hardest rounds test whether you can reason about trade-offs under load, not just recall definitions. Expect to write and optimize SQL against a real schema, debug or extend Python under time pressure, design systems that scale to millions of requests, and answer probing follow-ups on AWS service selection and cost.
Preparation should weight system design and SQL the heaviest, since those rounds carry the most signal at senior level. Python and HTTP/API questions are often the coding screen, while AI Engineering questions check whether you understand LLM integration patterns well enough to ship them responsibly.
| Competency area | What the round covers |
|---|---|
| Databases & SQL | Query optimization, indexing strategy, joins, window functions, transaction isolation |
| Python | Idiomatic patterns, concurrency (asyncio, threading), memory model, debugging |
| HTTP & APIs | REST and RPC design, idempotency, caching headers, auth flows, rate limiting |
| System Design | Scalability, data partitioning, consistency models, queue-based architectures |
| AI Engineering | RAG pipelines, prompt engineering, model evaluation, cost and latency trade-offs |
| AWS | Service selection (RDS vs DynamoDB, Lambda vs EC2), IAM, networking, cost control |
How should you sequence your study for a Senior Software Engineer interview?
Work through the areas in order of interview signal, not alphabetical convenience:
- Start with system design, because it is the round where senior candidates are separated from mid-level. Practice designing a real system end-to-end: capacity estimates, data model, component diagram, and a verbal trade-off discussion.
- Sharpen SQL and database internals next. Write queries that join three-plus tables, use window functions, and explain query plans. Know when an index helps and when it does not.
- Lock down Python coding fluency. You should be able to implement a class hierarchy, handle concurrent I/O, and spot a memory issue in under 20 minutes.
- Review HTTP fundamentals and API design patterns: status codes, idempotency keys, pagination, and caching.
- Brush up on AWS service selection and AI Engineering patterns together, since both rounds ask you to pick the right tool and justify it.
The live quiz below pulls real interview questions across all six areas, and the salary and demand data underneath reflects current market figures for the role.
What to study, in order
For a senior Software Engineer interview, prioritise the role's most in-demand technologies first:
- Databases & SQL
- Python
- HTTP & APIs
- System Design
- AI Engineering
- AWS
What do Software Engineer roles pay in 2026?
From live job postings, the median Software Engineer salary is £70,000, across 6,452 postings in August 2026. These figures are role-wide across all seniority levels, as of August 2026.
| 25th percentile | £55,000 |
|---|---|
| Median (50th percentile) | £70,000 |
| 75th percentile | £95,000 |
Advertised base salary, from 894 job postings with pay data, as of September 2026.
We need 3 complete months of tracking before we publish a month-by-month series. Months we only partly covered are left out rather than shown as low demand.
Practice questions
AI Engineering/agents/agent-loops
Place the steps of a single ReAct agent loop iteration in the correct order, starting from when the model receives the current context.#
Put these in order
- Model produces a Thought (internal reasoning chain)
- Model emits an Action (tool name + arguments)
- Environment executes the action and returns an Observation
- Observation is appended to the context and the loop checks for a final answer / termination condition
Show answer
- Model produces a Thought (internal reasoning chain)
- Model emits an Action (tool name + arguments)
- Environment executes the action and returns an Observation
- Observation is appended to the context and the loop checks for a final answer / termination condition
In a ReAct-style agent loop, the canonical cycle is: (1) Reason — the model produces a Thought about what to do next; (2) Act — the model emits a tool call/action based on that thought; (3) Observe — the environment executes the action and returns an observation; then the loop repeats with the new observation appended to the context until a final answer is produced. Skipping or reordering these steps breaks the grounding that makes ReAct reliable.
AWS/aws-storage/s3
Your service writes a brand-new object to S3 and, a millisecond later, issues a GET for that same key from another instance. What consistency does S3 guarantee for that read today?#
Options
- Strong read-after-write: the GET is guaranteed to return the just-written object
- Eventual consistency: the GET may return a 404 until replication settles
- Strong consistency only if you enable Versioning on the bucket
- Strong consistency only within a single Availability Zone
Show answer
S3 guarantees strong read-after-write consistency: a GET issued after a successful PUT always returns the latest object. Since December 2020 this applies automatically to all GET, PUT, LIST, and tag/ACL/metadata operations, in every Region, with no extra cost or configuration. The earlier eventual-consistency model — and the read-your-own-write workarounds built around it — no longer applies. It does not require Versioning and is not limited to one Availability Zone.
Since December 2020, S3 provides strong read-after-write consistency automatically for all GET, PUT, LIST, and tag/ACL/metadata operations, in every Region, at no extra cost. A read issued after a successful write always sees the latest data. The old eventual-consistency model (and the read-your-own-write workarounds people built around it) is gone — but a lot of legacy advice and interview prep still teaches it, which is the trap here. Versioning and AZ scope are unrelated to this guarantee.
Databases & SQL/db-performance/query-planning
A PostgreSQL table orders has 10 million rows. A B-tree index exists on the status column. The following query ignores the index and performs a sequential scan:#
Options
- The index statistics are stale, so the planner underestimates index selectivity and falls back to a sequential scan.
- The planner's cost model determines that fetching ~9.4 million heap rows via random index lookups is more expensive than a single sequential pass over the heap.
- PostgreSQL never uses a B-tree index on low-cardinality columns like
status; a partial index or GIN index is required. - The
fillfactorof the index is too high, causing excessive index page splits that inflate the estimated index scan cost.
Show answer
The planner correctly chooses a sequential scan because fetching ~9.4 million rows via random index lookups would be far more expensive than one sequential pass over the heap. PostgreSQL's cost model weights random I/O (random_page_cost) higher than sequential I/O (seq_page_cost), so when a predicate matches a large fraction of rows the index offers no benefit.
PostgreSQL's query planner chooses between a sequential scan and an index scan based on cost estimates derived from statistics (pg_statistic). When a predicate matches a large fraction of rows (e.g., a low-selectivity condition like status = 'active' on a table where 95% of rows are active), the planner correctly decides that reading the entire heap sequentially is cheaper than random index lookups plus heap fetches. The planner uses the page-level cost model: random I/O has a higher cost constant (random_page_cost) than sequential I/O (seq_page_cost). An index is only beneficial when selectivity is high (few rows returned). Options about outdated statistics (ANALYZE not run) and fillfactor are distractors — the described behavior is correct planner behavior, not a bug.
HTTP & APIs/api-design/webhooks
You are designing a webhook consumer endpoint that receives order-created events from a third-party payment provider. The provider retries delivery up to 5 times with exponential back-off if it does not receive an HTTP 2xx within 10 seconds. Processing each event involves writing to a database and sending an email, which can take up to 30 seconds.#
Options
- Process the database write and email inline, then return
200 OK. Add a unique constraint on the event ID to prevent duplicates caused by retries. - Verify the HMAC signature, process everything inline, and return
200 OK. Retries are only a concern if the signature check fails. - Verify the HMAC signature, persist the raw payload to a durable queue/store, return
200 OKimmediately, and process asynchronously — using the event ID to deduplicate before acting. - Return
202 Acceptedimmediately without any verification, then process inline in a background thread within the same HTTP request lifecycle.
Show answer
The correct strategy is to verify the webhook's HMAC signature, persist the raw payload to a durable queue or store, return 200 OK right away, and process the event asynchronously — using the event ID to deduplicate before taking any action. This matters because the provider expects a 2xx within its 10-second timeout window, but the actual work (database write, email send) can take up to 30 seconds. If you do that work synchronously, the provider times out and retries, producing duplicate deliveries. Storing the raw payload first and processing it later is the standard accept-then-process pattern; checking the event ID before acting prevents duplicate side effects across retries.
Webhook reliability requires the consumer to respond quickly (within the provider's timeout window, often 5–30 s) with a 2xx status to acknowledge receipt, then process asynchronously. If the consumer does heavy work synchronously and times out, the provider may retry, causing duplicate deliveries. Idempotency keys/event IDs let consumers deduplicate retries. Signature verification (HMAC) guards against spoofed payloads but does not help with duplicate delivery. Storing the raw payload before processing is the correct 'accept-then-process' pattern. Option C describes returning a 200 immediately and queuing the work — the correct pattern — while the others describe anti-patterns or incomplete solutions.
Python/typing/type-hints
What is printed by the following code? Assume the module is run as __main__ and Node is fully defined before get_type_hints is called.#
from __future__ import annotations
from typing import Optional, get_type_hints
class Node:
def __init__(self, val: int, next: Optional[Node] = None) -> None:
self.val = val
self.next = next
hints = get_type_hints(Node.__init__)
print(hints['next'])Show answer
typing.Optional[__main__.Node]
Python's typing.get_type_hints() resolves forward references and applies __future__ annotations. When from __future__ import annotations is active, ALL annotations are stored as strings (PEP 563 postponed evaluation). get_type_hints() then resolves them at call time using the provided (or inferred) global namespace. Here the forward reference 'Node' is resolved by get_type_hints() to the actual Node class, so hints['next'] returns typing.Optional[Node]. Printing that gives typing.Optional[__main__.Node] (or the equivalent module path). The __annotations__ dict, by contrast, would still hold the raw string.
System Design/sd-architecture
You are designing a globally distributed, multi-region active-active database (e.g., a shopping-cart service). Each region accepts both reads and writes and replicates asynchronously to the others. A user updates their cart from two different regions nearly simultaneously before either update has replicated.#
Options
- Last-Write-Wins (LWW) using each node's local wall-clock timestamp
- Two-phase commit (2PC) coordinated across all regions before acknowledging each write
- Vector clocks (or version vectors) attached to each write, with application-level or CRDT-based merge on conflict detection
- Read repair triggered at query time to reconcile diverged replicas
Show answer
Vector clocks (or version vectors) with application-level or CRDT-based merging are the best fit. They capture causal relationships between writes across regions, allowing the system to detect true conflicts (concurrent writes with no causal ordering) and apply deterministic merge logic — without requiring cross-region coordination on the write path. LWW risks silent data loss due to clock skew, 2PC sacrifices availability, and read repair only acts on the read path.
In a multi-region active-active architecture, the core challenge with eventual consistency is handling concurrent writes to the same record from different regions. Vector clocks (or similar mechanisms like CRDTs) track causality across nodes so that conflicts can be detected and resolved rather than silently overwriting data. Last-Write-Wins (LWW) based solely on wall-clock time is unsafe because clocks can skew, making it possible to overwrite a newer write with an older one. Two-phase commit provides strong consistency but introduces cross-region coordination latency and is an availability risk — the opposite of what active-active targets. Read repair is a read-path technique and does not prevent write conflicts. Vector clocks remain the standard causality-tracking answer for active-active replication conflict detection.
AI Engineering/agents/agent-memory
An AI agent framework separates memory into four tiers: in-context (working) memory, episodic/external memory (vector store), semantic memory (knowledge base), and procedural memory (learned skills/tool policies).#
Options
- In-context memory is bounded by the LLM's context-window length and is lost between independent agent sessions unless explicitly reconstructed.
- Episodic memory stored in a vector database allows an agent to retrieve semantically similar past observations without reprocessing the entire history in the prompt.
- Procedural memory is best represented as a large JSON object appended to every system prompt so the agent can recall its own tool-call patterns.
- Semantic memory (e.g., a knowledge graph or document store) can be updated at runtime without retraining the underlying model weights.
- In-context memory persists transparently across independent agent sessions because transformer attention caches the KV state on the server.
Show answer
The correct statements are: (a) in-context memory is bounded by context length and is lost between sessions unless rebuilt; (b) a vector-DB episodic store enables efficient semantic retrieval without stuffing full history into the prompt; and (d) semantic memory in an external store can be updated at runtime without touching model weights. Procedural memory encoded as a JSON blob in every prompt is wasteful and brittle, and KV caches are not session-persistent across independent API calls.
Agent memory architectures are typically decomposed into four distinct tiers that serve different temporal and functional purposes. In-context (working) memory is the active prompt window — cheap to read but bounded by context length. External/episodic memory (e.g., a vector store of past episodes) allows retrieval across unlimited history but requires an explicit lookup step. Semantic/knowledge memory stores structured facts the agent can query. Procedural memory encodes learned skills or tool-use policies, often baked into weights or fine-tuned adapters — NOT the in-context window, which is stateless across sessions. The statement that in-context memory persists across independent agent sessions is false: a fresh invocation starts with an empty context unless explicitly reconstructed from an external store.
AWS/aws-databases/dynamodb
You need a strongly consistent read in DynamoDB (read the latest committed write). Select all access patterns where a strongly consistent read is actually available.#
Options
GetItemon a base table withConsistentRead: true- A
Queryon a local secondary index (LSI) withConsistentRead: true - A
Queryon a global secondary index (GSI) - A read from a DynamoDB Stream
Show answer
Strongly consistent reads are available only on a base table and on local secondary indexes, opted into per request with ConsistentRead: true (reads are eventually consistent by default). Global secondary indexes keep their own asynchronously-replicated copy and are eventually consistent only — requesting a strong read on a GSI is rejected. DynamoDB Streams deliver change records eventually, not as consistent point reads. Routing a read path through a GSI when you need the latest write is the common design mistake.
Strongly consistent reads are supported only on base tables and local secondary indexes, because an LSI shares the partition's storage with the table. Reads default to eventually consistent; you opt into strong consistency per-request with ConsistentRead: true. Global secondary indexes (c) maintain their own asynchronously-updated copy of the data, so they are eventually consistent only — passing ConsistentRead: true to a GSI query is an error. DynamoDB Streams (d) deliver change records eventually, not as consistent point reads. The GSI limitation is the classic gotcha: people design a read path through a GSI and then discover they can't get a strong read there.
Databases & SQL/schema-design/normalization
Consider relation R(A, B, C, D) with the following functional dependencies (FDs):#
Options
- 1NF only
- 2NF but not 3NF
- 3NF but not BCNF
- BCNF
- 4NF
Show answer
The relation R satisfies 3NF but not BCNF. The dependency C → B violates BCNF because C is not a superkey (C⁺ = {C, B}, which does not cover all attributes). However, it satisfies 3NF because B is a prime attribute (part of candidate key AB), exempting C → B from the 3NF violation rule that applies only when the right-hand side is a non-prime attribute.
A relation is in BCNF if for every non-trivial functional dependency X → Y, X is a superkey. The given relation R(A, B, C, D) has FDs: AB → C, C → B, and AB → D. Check each: AB → C: AB is a superkey? AB → C and AB → D means AB determines all attributes (AB → ABCD), so yes, AB is a superkey. AB → D: same reasoning, valid. C → B: Is C a superkey? C+ = {C, B} ≠ {A,B,C,D}, so C is NOT a superkey. Therefore C → B violates BCNF. This relation is in 3NF (because B is a prime attribute — it's part of the candidate key AB), but NOT in BCNF. The relation is NOT in 4NF either because it first fails BCNF, and 4NF requires BCNF. The correct answer is 3NF but not BCNF.
HTTP & APIs/api-design
A public REST API currently returns the following response for GET /users/{id}:#
Options
- Renaming the field
full_nametoname - Adding a new optional field
avatar_urlto the response - Removing the field
internal_scorefrom the response - Changing the type of
idfrom integer to UUID string (e.g.,"id": "a1b2-...") - Returning HTTP 200 with an empty object
{}instead of HTTP 404 when the user is not found
Show answer
The breaking changes that call for a new major version are: renaming full_name to name, removing internal_score from the response, changing id from an integer to a UUID string, and returning a 200 with {} instead of a 404 when a user is not found. Each of these can make a correctly written client behave differently without any code change — a field rename or removal breaks readers of the old key, the type swap on id breaks numeric handling, and the status change breaks clients that branch on 404 to detect a missing resource. Adding a new optional field, by contrast, is an additive, non-breaking change under most REST contracts.
Breaking changes are those that can cause existing well-written clients to behave incorrectly without modification. (a) Renaming full_name to name breaks clients that read full_name. (c) Removing internal_score breaks clients that depend on it. (d) Changing id's type from integer to string breaks clients that store or compare it as a number. (e) Returning 200 instead of 404 for a missing resource breaks clients that branch on HTTP status codes to detect absence. Adding a new optional field (b) is generally considered a non-breaking, additive change under most REST versioning contracts.
Python/testing-idioms/pytest
You have a pytest test that should be skipped when the environment variable CI is not set OR is set to an empty string (i.e., when CI is falsy). Which decorator achieves this correctly?#
Options
- Option A —
os.getenv("CI") is None: skips only whenCIis completely absent, but runs whenCI="" - Option B —
not os.getenv("CI"): skips whenCIis absent or set to an empty string, treating both as 'not usefully set' - Option C —
@pytest.mark.skip: unconditionally skips the test regardless of any environment variable - Option D —
os.getenv("CI") is not None: skips whenCIis set, which is the opposite of the intended behaviour
Show answer
The correct decorator is @pytest.mark.skipif(not os.getenv("CI"), reason="Only run in CI"). pytest.mark.skipif skips the test when its condition is True, and not os.getenv("CI") is True both when CI is unset and when it is set to an empty string. Checking is None would miss the empty-string case, since os.getenv("CI") returns "" rather than None when CI is exported as empty.
pytest.mark.skipif skips the test when its condition evaluates to True. The requirement here is to skip when CI is falsy — meaning absent or set to an empty string (e.g., CI=). not os.getenv("CI") is True in both of those cases, making Option B correct. Option A (is None) only returns True when the variable is entirely absent; if CI is exported as an empty string, os.getenv("CI") returns "" (not None), so is None is False and the test would run despite CI being empty — missing the falsy case. Option D inverts the logic and skips when CI is present. Option C unconditionally skips, ignoring the environment variable entirely.
System Design/sd-fundamentals/scalability
A team is horizontally scaling a stateless web service behind a round-robin load balancer. They notice that certain user sessions break when requests land on different instances, so they enable sticky sessions (session affinity) at the load balancer.#
Options
- Sticky sessions increase TLS handshake overhead; fix by terminating TLS at the load balancer instead of at origin servers.
- Sticky sessions re-couple users to specific instances, re-introducing per-instance state and uneven load distribution; fix by externalising session state to a distributed cache (e.g., Redis) so any instance can serve any request.
- Sticky sessions bypass the load balancer's health checks; fix by implementing application-level heartbeat endpoints on each instance.
- Sticky sessions cause cache thundering-herd on cold starts; fix by pre-warming each instance with a read-through cache before it receives traffic.
Show answer
Sticky sessions re-introduce per-instance state, unevenly distributing load and creating a soft single point of failure per user — defeating horizontal scalability. The canonical fix is to externalise session state to a shared distributed cache (e.g., Redis or Memcached) so the service remains truly stateless and any instance can handle any request without affinity.
In a horizontally scaled stateless service behind a load balancer, session affinity (sticky sessions) pins a user's requests to a single instance. This re-introduces a single point of failure per user and prevents full horizontal elasticity. The correct alternative is to externalise session state into a shared distributed store (e.g., Redis) so any instance can serve any request. Rate-limiting at the LB, distributing static assets via CDN, and database read replicas are all orthogonal scalability improvements that do not conflict with statelessness.
AI Engineering/evaluation-safety/guardrails
You are building an automated evaluation pipeline that uses GPT-4 as an LLM-as-a-judge to score candidate model responses on a benchmark. Which of the following are documented, systematic biases that LLM judges exhibit and that you must account for when designing this pipeline?#
Options
- Position bias — the judge consistently assigns higher scores to the response presented first in a pairwise comparison.
- Verbosity bias — the judge tends to prefer longer responses even when they are not more accurate or helpful.
- Self-enhancement bias — a judge model from a given model family rates outputs of the same family disproportionately higher.
- Calibration drift — the judge's absolute score scale shifts unpredictably between API calls due to temperature sampling.
- Sycophancy leakage — the judge inflates scores whenever the candidate response agrees with the judge's own prior outputs.
Show answer
The three documented systematic biases are position bias (higher scores for the first-presented answer), verbosity bias (longer answers rated higher regardless of quality), and self-enhancement bias (same-family model outputs rated more favourably). These are empirically demonstrated in the MT-Bench and Chatbot Arena literature. Calibration drift from temperature sampling and sycophancy leakage are not the same phenomenon — they describe target-model behaviour, not judge-model structural bias.
LLM-as-a-judge is a popular evaluation technique, but it has well-documented systematic biases. Studies (e.g., from Zheng et al. 2023 on MT-Bench) show that GPT-4 and similar judges exhibit (1) position bias — preferring the first answer shown, (2) verbosity bias — favouring longer answers regardless of correctness, and (3) self-enhancement bias — rating outputs from models of the same family higher. Calibration drift and sycophancy are real but are properties of the target model, not the judge. The key insight for senior engineers is that using a single LLM judge without positional swapping, length normalization, or multi-judge consensus will silently skew evaluation results.
AWS/aws-integration/sqs
A standard SQS queue can deliver the same message more than once, so consumers of a standard queue should be written to handle duplicate processing idempotently.#
Options
- True
- False
Show answer
True. Standard SQS queues provide at-least-once delivery — because messages are stored redundantly across servers, a transient failure during a delete can cause the same message to be received again, so consumers must process messages idempotently. FIFO queues are the option that suppresses duplicates and preserves order (exactly-once processing) at the cost of throughput. Treating a standard queue as exactly-once is a common cause of double-applied side effects like duplicate charges.
True. Standard queues guarantee at-least-once delivery: because SQS stores messages redundantly across servers, a server being briefly unavailable during a delete can cause the same message to be received again. So consumers must be idempotent. If you need duplicates suppressed and strict ordering, you use a FIFO queue, which provides exactly-once processing and ordered delivery (at lower throughput). Assuming a standard queue delivers exactly once is a frequent source of double-charged payments and duplicated side effects in production.
Databases & SQL/transactions/acid
Which ACID property is directly responsible for defining the four standard isolation levels (READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE), and what problem does each level mitigate?#
Options
- Atomicity — it ensures partial writes from concurrent transactions are rolled back, defining how much of another transaction's work is visible.
- Consistency — it ensures concurrent transactions always leave the database in a valid state, which requires progressively stricter rules about what data each transaction can read.
- Isolation — it controls the degree to which an in-progress transaction is shielded from changes made by other concurrent transactions, addressing anomalies such as dirty reads, non-repeatable reads, and phantom reads.
- Durability — it ensures that once committed data is visible to all transactions, the engine must choose how long uncommitted data remains hidden from others.
Show answer
Isolation is the ACID property that controls how concurrent transactions interact with each other's uncommitted data. The four standard isolation levels (READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE) are all refinements of this guarantee, each trading off anomaly protection (dirty reads, non-repeatable reads, phantom reads) against concurrency throughput.
ACID's Isolation property is the one that governs how concurrent transactions see each other's in-progress changes. The four standard isolation levels (READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE) are all refinements of the Isolation guarantee. Atomicity ensures all-or-nothing completion; Consistency ensures data remains valid after a transaction; Durability ensures committed changes survive crashes. None of the other three properties concern themselves with concurrency anomalies like dirty reads, non-repeatable reads, or phantoms — those are isolation concerns.