Mid-level Software Engineer interview prep questions

Reviewed by Mark Dickie · Last updated

Mid-level Software Engineer interviews are evaluations of your ability to ship production code and design systems that hold up under real load. Interviewers at this level expect more than correct code. They want you to reason about trade-offs in schema design, API contracts, and cloud deployment. Expect a mix of live coding, SQL problem-solving, system design whiteboarding, and questions that probe your familiarity with AI-assisted development workflows.

You'll face questions across six competency areas, each listed with what it covers:

AreaWhat it covers
Databases & SQLSchema design, query optimization, indexing, joins, window functions
PythonIdiomatic patterns, data structures, error handling, standard library
HTTP & APIsREST principles, status codes, authentication, request/response lifecycle
System DesignService architecture, caching, queues, load balancing, failure handling
AI EngineeringLLM integration, prompt engineering, RAG pipelines, evaluation methods
AWSCore services (EC2, S3, RDS, Lambda), IAM, networking, cost basics

What should I study first for a Mid-level Software Engineer interview?

The order matters because later topics build on earlier ones. Work through them in sequence:

  1. Databases & SQL: solidify joins, indexing, and query plans first, since data modeling decisions show up in system design and coding rounds alike.
  2. Python: sharpen your command of the standard library and common patterns so live coding feels automatic.
  3. HTTP & APIs: learn the request lifecycle and REST conventions, which underpin both backend coding tasks and system design discussions.
  4. System Design: practice breaking a problem into services, choosing data stores, and defending your caching and scaling choices out loud.
  5. AI Engineering: understand how LLMs fit into production systems, from prompt design to retrieval-augmented generation to evaluation.
  6. AWS: map the core services to the system design components you've already been sketching.

What salary and demand data exists for Mid-level Software Engineers?

Salary and hiring-demand numbers for this role are rendered below from current market data, scoped to the technologies and seniority level this guide targets. Use them alongside your prep to calibrate what to expect when offers come in.

What to study, in order

For a mid-level Software Engineer interview, prioritise the role's most in-demand technologies first:

  1. Databases & SQL
  2. Python
  3. HTTP & APIs
  4. System Design
  5. AI Engineering
  6. 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

Which of the following best describes the core cycle of a ReAct-style agent loop?#

Options

  • Receive user input → Tokenize → Embed → Return embedding
  • Observe → Reason (think) → Act (call tool) → Observe (repeat until done)
  • Pre-train → Fine-tune → RLHF → Deploy
  • Encode prompt → Sample tokens → Decode → Cache KV states (repeat)
Show answer

A ReAct-style agent loop runs a repeating cycle: observe the current state, reason about what to do next, act by selecting and calling a tool, then observe the result and repeat until a stopping condition is met — such as emitting a final answer or hitting a max-step limit. Training the model is an offline, one-time activity and is not part of this runtime loop.

Why:

An agent loop (also called a ReAct-style or think-act loop) follows a fixed cycle: the agent receives an observation, reasons/thinks about it, selects and calls a tool/action, then receives the next observation. This cycle repeats until a stopping condition is met (e.g., the agent emits a final answer or a max-step limit is reached). 'Train the model' is a one-time offline activity, not part of a runtime agent loop.

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.

Why:

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/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 fillfactor of 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.

Why:

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 OK immediately, and process asynchronously — using the event ID to deduplicate before acting.
  • Return 202 Accepted immediately 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.

Why:

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]
Why:

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/microservices

In a microservices architecture, Service A needs to call Service B to complete a user request. Service B is temporarily unavailable. Which pattern is most appropriate to prevent Service A from exhausting its own resources (e.g., thread pools) while Service B is down?#

Options

  • Retry pattern — keep retrying Service B with exponential back-off until it responds.
  • Circuit Breaker pattern — detect repeated failures and stop forwarding calls to Service B until it recovers.
  • Saga pattern — coordinate a series of local transactions to undo side effects across services.
  • Sidecar pattern — deploy a proxy container alongside Service A to handle all network traffic.
Show answer

The Circuit Breaker pattern is the most appropriate choice. It tracks failure rates to Service B and, once a threshold is exceeded, 'opens' the circuit so subsequent calls fail fast instead of blocking. This prevents Service A's thread pools from being exhausted waiting for a slow or unreachable dependency, allowing the system to degrade gracefully rather than cascade-fail.

Why:

The Circuit Breaker pattern monitors call failures to a downstream service and, after a threshold is crossed, 'opens' the circuit so that further calls fail immediately rather than waiting for a timeout. This prevents thread pools and other resources in the calling service from being exhausted while the dependency is down. Retries alone worsen the situation by consuming more resources. The Saga and Sidecar patterns address different concerns (distributed transactions and cross-cutting concerns, respectively).

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.

Why:

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-iam-security/iam-policies

This identity policy is meant to let a service read and write objects in only the reports-prod bucket. A security review flags it. What is the real problem?#

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "*"
    }
  ]
}

Options

  • Resource: "*" grants the actions on every object in every bucket, not just reports-prod — it should be arn:aws:s3:::reports-prod/*
  • Version is wrong; an S3 policy must use "Version": "2025-01-01"
  • s3:GetObject and s3:PutObject cannot appear in the same statement
  • The policy is missing an explicit Allow for s3:*, so nothing is permitted
Show answer

Resource: "*" grants the actions on every object in every bucket, not just reports-prod — it should be arn:aws:s3:::reports-prod/*

Why:

The actions are scoped correctly, but Resource: "*" applies them to every object in every bucket in the account — a massive over-grant that violates least privilege. The fix is the bucket's object ARN, arn:aws:s3:::reports-prod/* (and arn:aws:s3:::reports-prod for bucket-level actions if needed). The Version date 2012-10-17 is the current, correct policy-language version — it is not a year you bump. Multiple actions in one statement is normal, and you would never broaden to s3:* to fix an over-permissive policy. Over-broad Resource is one of the most common and dangerous real-world IAM mistakes.

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.

Why:

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_name to name
  • Adding a new optional field avatar_url to the response
  • Removing the field internal_score from the response
  • Changing the type of id from 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.

Why:

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 when CI is completely absent, but runs when CI=""
  • Option B — not os.getenv("CI"): skips when CI is 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 when CI is 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.

Why:

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-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.

Why:

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/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.

Why:

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-networking/security-groups

A teammate's Terraform for a production bastion host includes this security-group ingress rule. The security scanner fails the build. What is the issue?#

ingress {
  description = "SSH"
  from_port   = 22
  to_port     = 22
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

Options

  • It opens SSH (port 22) to the entire internet (0.0.0.0/0); it should be restricted to known admin CIDRs or fronted by SSM Session Manager
  • from_port and to_port must differ; a single-port rule is invalid
  • protocol = "tcp" is wrong for SSH; SSH requires protocol = "ssh"
  • Security groups cannot use CIDR blocks; ingress must reference another security group
Show answer

It opens SSH (port 22) to the entire internet (0.0.0.0/0); it should be restricted to known admin CIDRs or fronted by SSM Session Manager

Why:

cidr_blocks = ["0.0.0.0/0"] on port 22 exposes SSH to every IP on the internet, inviting brute-force and credential-stuffing attacks — a textbook misconfiguration scanners flag. Restrict it to specific admin/VPN CIDRs, or better, drop public SSH entirely and use AWS Systems Manager Session Manager (no open inbound port at all). The other options are wrong: a single-port rule with equal from_port/to_port is valid, the protocol field takes tcp/udp/etc. (not ssh), and security groups absolutely can use CIDR blocks as well as referencing other groups. Wide-open management ports are among the most common cloud breaches.

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.

Why:

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.

Keep reading

What moved, monthly

One email a month when the bulletin comes out: what moved in the markets we track, and the new question topics we published. Confirm your address to join. Unsubscribe any time.