Interview guides · Gumtree
Gumtree Software Engineer Interview Guide
Gumtree Software Engineer interview guide: reported rounds, questions and how candidates describe the process.
The 60-second brief
As a Software Engineer at Gumtree, you are at the heart of one of the most established online marketplaces. Your work directly influences how millions of users buy, sell, and connect within their local communities. This role is not just about writing code; it is about building scalable, reliable features that manage high-traffic volumes and ensure a seamless user experience across a diverse set of categories. The engineering culture at Gumtree is centered on pragmatism and impact. You will likely work within agile, cross-functional teams, collaborating closely with product owners and designers to translate user needs into technical solutions. Whether you are optimizing existing services, implementing new microservices, or tackling complex debugging challenges, your contributions will have a tangible, immediate effect on the platform’s performance and business goals.
This guide is scoped to a Software Engineer candidate at Gumtree.
Focus areas
- Spring Boot
- Hibernate (ORM)
- RESTful APIs
The Gumtree interview process
Gumtree candidates report 3 rounds over 3-5 weeks. The stages below are what candidates describe, not a published process.
Recruiter Screen
Initial discussion to align on your background and motivations.
What to demonstrate
- Initial discussion to align on your background and motivations
- Depth in Spring Boot
How to prepare
- Be able to walk your CV end to end in two minutes, and say why this company specifically.
- Have your salary expectations, notice period and location constraints ready, and ask for the rest of the loop in writing.
Technical Component
Includes a take-home task or a live coding/pairing session.
What to demonstrate
- Includes a take-home task or a live coding/pairing session
- Depth in Spring Boot
How to prepare
- Answer aloud and timed: What happens under the hood when you type a URL into a browser and hit enter?
- Answer aloud and timed: What are the core principles of RESTful API design?
On-site/Virtual Interview
Final interview with multiple team members to assess technical depth and cultural fit.
What to demonstrate
- Final interview with multiple team members to assess technical depth and cultural fit
- Depth in Spring Boot
How to prepare
- Answer aloud and timed: Can you explain the SOLID principles and their importance in software development?
- Answer aloud and timed: What is a microservices architecture, and what are its trade-offs?
Where candidates lose points
Be ready to talk about your code
If you submit a take-home task, expect to be grilled on your implementation, testing choices, and potential improvements. Be prepared to defend your decisions.
Prepare for "whiteboard" style debugging
You may be asked to fix code while an engineer watches. Practice talking through your thought process aloud to avoid long, awkward silences.
Know your stack deeply
Whether it is Java or JavaScript, be prepared to answer questions about memory management, concurrency, and standard design patterns.
Research the product
Understand Gumtree as a user. Having a clear idea of how the platform works and where it could be improved shows genuine interest.
Questions & practice
How does JavaScript manage memory, and how would you detect or fix a memory leak?
Medium · Technical Foundations and Domain Knowled
Approach
- Allocation is automatic: objects, arrays, closures and strings live on the heap, and the garbage collector reclaims anything no longer reachable from the roots (the global object and the variables on the active call stack), directly or through a chain of references.
- Modern engines use tracing mark-and-sweep, not reference counting, so unreachable cycles are collected. V8 is generational: a fast scavenger for the young generation (most objects die young) and concurrent mark-sweep-compact for the old generation.
- A leak in a garbage-collected language is memory that is still reachable but no longer needed. Usual causes: listeners or
setIntervalcallbacks never removed, closures holding large objects, detached DOM nodes kept in a variable, unbounded caches or Maps, accidental globals. - Confirm the trend before hunting: heap grows across repeated identical actions and stays high after a forced GC. Then take three heap snapshots around repeated actions in Chrome DevTools and use the Comparison view, or record Allocation instrumentation on timeline to see what survives.
- Follow the retainer chain in the snapshot to the reference keeping the object alive and fix it at the owner: remove listeners and clear timers on teardown (
useEffectcleanup,AbortController), bound caches with LRU eviction, and useWeakMapfor data keyed by objects you don't own. - Verify by repeating the scenario and showing the heap returns to a flat baseline, then guard it with a soak test or memory alert. Two misconceptions that weaken an answer: 'GC means no leaks' and 'setting a variable to
nullfrees memory immediately'.
Follow-up
- Why isn't reference counting enough? Two objects that reference each other never drop to a count of zero; tracing from the roots collects unreachable cycles regardless of internal references.
- When would you use a
WeakMapinstead of aMap? When attaching data to objects you don't own; an entry does not keep its key alive, so it disappears when the key object is collected. - How would you find a leak in a Node.js server? Track heap used and RSS over time, capture snapshots on a canary with
--heapsnapshot-signalorv8.writeHeapSnapshot(), and diff them to find growing object types.
Can you explain the event loop and the concept of closures?
Medium · Technical Foundations and Domain Knowled
Approach
- JavaScript runs your code on one thread with one call stack; the event loop lets it handle I/O and timers without blocking. Async work (network, timers, file I/O) is handed to the host, the browser or libuv in Node, which queues a callback when the work completes.
- Loop order: run the current task until the stack is empty, drain the entire microtask queue (Promise reactions,
queueMicrotask,awaitcontinuations), let the browser render if due, then take the next task (timers, I/O, UI events). - Prove it with a snippet:
setTimeout(() => log('a'), 0); Promise.resolve().then(() => log('b')); log('c')printsc b a. Long synchronous work, or microtasks that keep queueing microtasks, starve rendering and timers; chunk the work or move it to a Web Worker. - A closure is a function bundled with the lexical environment it was created in, so it keeps access to outer variables after the outer function has returned. It captures the variable binding, not a snapshot of its value.
- Show uses and the classic trap: private state (
makeCounter), function factories, memoisation, callbacks.for (var i = 0; i < 3; i++) setTimeout(() => log(i))prints3 3 3because all callbacks share onevar i;letmakes a fresh binding per iteration and prints0 1 2. - Tie the two together: callbacks the event loop runs later depend on closures to remember their context, which is also why a closure in a listener that is never removed keeps everything it references alive.
Follow-up
- Where does
process.nextTickfit in Node? After each callback Node drains thenextTickqueue before Promise microtasks, but an ES module's top-level body already runs as a microtask, so a.thenqueued there fires first. - Does
awaitblock the thread? No; it suspends only that async function and schedules the rest as a microtask when the promise settles, so other code keeps running meanwhile. - How do closures cause stale state in React hooks? A callback created in an earlier render closes over that render's values; use functional state updates or correct dependency arrays.
Given an incomplete project, can you finish functionalities and optimize the existing test cases?
Medium · Coding and Problem-Solving
Approach
- Interpretation: you get a partly built codebase with a test suite and must complete missing features and improve the tests. Start by running it as-is: install, build, run every test, and record the baseline of what passes, what fails and how long the suite takes.
- Read before writing: the brief and README, the existing tests (the best available spec),
TODOorNotImplementedErrormarkers, and the project's conventions. Where requirements are ambiguous, ask or write the assumption down rather than guessing silently. - Finish features in thin, test-first slices: take one missing behaviour, write or un-skip a failing test for it, implement the minimum to pass, refactor, and commit with a clear message. Match the existing style rather than re-architecting.
- Improve the existing tests where they are weakest: check coverage of the code you touched for untested branches, strengthen assertions that prove nothing (a bare
assert result, or none at all), delete duplicate tests, and run in random order to expose order-dependent ones. - Never make a red test green by weakening its assertion unless you can show the test was wrong, and say so in the commit. Quantify the improvement: suite runtime before and after, flaky tests removed, new cases covered.
- Finish the features the brief or existing tests exercise first, and leave a skipped test naming each gap you could not close. End with a short README note on features added, tests changed and why, assumptions made, and what remains.
Follow-up
- What if an existing test contradicts the brief? Confirm which one is right, fix the test or the code accordingly, and record the decision instead of silently changing either.
- How would you speed up a slow test suite? Profile the slowest tests, replace I/O with fakes, scope expensive fixtures correctly, run tests in parallel, and move true integration tests to a separate stage.
- How do you change untested legacy code safely? Add characterisation tests that pin down current behaviour first, then refactor or extend under that safety net.
Stop tag and share joins from fanning out a page
Medium · join fan-out · semi-joins · many-to-many
resource_tag is (resource_id, tag_id) with PK (resource_id, tag_id); resource_share is (resource_id, shared_with_user_id, permission). The tagged-and-shared listing inner-joins resource to both, filters tenant_id, tag_id = ANY($2) and shared_with_user_id = $3, orders by updated_at DESC and takes 50. Pages come back with fewer than 50 distinct resources and the total in the header is far too high. Explain the row multiplication, rewrite both the page query and the count query so each is correct, and name the index each one needs. PostgreSQL 16.
Approach
- Do the arithmetic against the predicates that are actually there. An inner join emits one row per matching child row, and both joins are filtered: tag_id = ANY($2) admits only the requested tags, shared_with_user_id = $3 admits one user's share rows. So a resource holding three of the requested tags and shared with $3 once yields three rows, not one — the multiplier is its count of matching tags times its share rows for that single user, and that second factor is 1 unless the table admits duplicate (resource_id, shared_with_user_id) pairs. LIMIT 50 then limits rows rather than resources, and COUNT(*) counts pairs — the header is the product, not the population.
- Reject DISTINCT as the fix. It deduplicates after the product has been built, so the planner must materialise and sort the fanned-out set before the LIMIT can apply, and it leaves any SUM or AVG in the same select list wrong.
- Rewrite both filters as semi-joins, keeping resource as the only row source: AND EXISTS (SELECT 1 FROM resource_tag rt WHERE rt.resource_id = r.resource_id AND rt.tag_id = ANY($2)) and the same shape against resource_share. A semi-join stops at the first match per resource and preserves the driving index order, so ORDER BY updated_at DESC, resource_id DESC LIMIT 50 still stops after 50 rows.
- Count with the same predicates and no join at all: SELECT count(*) FROM resource r WHERE r.tenant_id = $1 AND r.status = 'active' AND EXISTS (...) AND EXISTS (...). Nothing multiplies a resource, so the number is the population.
Follow-up
- The filter changes from 'any of these tags' to 'all of these tags'. Rewrite it and state what it costs relative to the ANY form.
- A resource can be shared with the same user twice under different permissions. Does your count change, and should it?
Write the update path that detects a concurrent edit
Medium · optimistic concurrency · transactional outbox · audit history
resource carries version INT NOT NULL DEFAULT 1. resource_revision holds revision_id, resource_id, version, actor_user_id, change_kind, patch JSONB, request_id, created_at with UNIQUE (resource_id, version). outbox_event holds aggregate_type, aggregate_id, aggregate_version, event_type, payload, status. A PUT carries the version the client read. Write the exact statements for the single transaction that applies the edit, records the revision and enqueues 'resource.updated', and give the handler's branch on zero affected rows. Then say what PostgreSQL 16 does under READ COMMITTED when two of these updates hit one row at once.
Approach
- One transaction, three writes, no network call inside it: UPDATE resource SET title = $3, version = version + 1, updated_at = now() WHERE resource_id = $1 AND tenant_id = $4 AND version = $2; then INSERT the resource_revision row at version $2 + 1; then INSERT the outbox_event row at the same aggregate_version. The event goes to a table rather than a broker because no transaction spans both.
- Branch on the affected-row count before doing anything else. Zero has three causes — stale version, wrong tenant, row gone — so re-read once and map to 409 carrying the current version, or 404 for an id outside the caller's tenant, which also stops the endpoint confirming that another tenant's id exists.
- State the engine behaviour instead of assuming it. Under READ COMMITTED the second UPDATE blocks on the row lock, and when the first commits PostgreSQL re-evaluates the WHERE clause against the newly committed row, so the version predicate now fails and the statement reports zero rows. Under REPEATABLE READ the identical collision raises SQLSTATE 40001 instead, so the handler must fold both shapes into one conflict response.
- Keep UNIQUE (resource_id, version) even though the predicate already serialises writers. It is what makes a lost update unwritable if any other path ever reaches the revision table, and it converts a logic bug into 23505 rather than into a silently missing history row.
Follow-up
- A client sends the version it read ten minutes ago and the resource has moved three versions. What is in your 409 so it can resolve the conflict without a full re-fetch?
- Two editors, two disjoint fields, no overlap. Does your answer still refuse the second write, and should it?
What happens under the hood when you type a URL into a browser and hit enter?
Medium · Technical Foundations and Domain Knowled
Approach
- Say you'll walk the happy path for
https://example.comand go deeper wherever steered. The browser first decides whether the input is a URL or a search, upgrades to HTTPS if the host is on its HSTS list, and may answer from a service worker or its HTTP cache without any network. - DNS: check the browser cache, then the OS cache and hosts file, then ask the configured recursive resolver, which answers from cache or walks root, TLD and authoritative name servers and returns A/AAAA records with a TTL.
- Connection: TCP three-way handshake (SYN, SYN-ACK, ACK), then a TLS 1.3 handshake that agrees keys and verifies the certificate chain against trusted CAs, with ALPN picking HTTP/2. HTTP/3 instead runs over QUIC on UDP, folding transport and TLS setup together.
GET /names the host (theHostheader in HTTP/1.1, the:authoritypseudo-header in HTTP/2 and 3), carries cookies andAccept-Encoding, and may pass a CDN, load balancer and proxy to an app server. The response brings a status,Cache-Control,Set-Cookieand a body; a 3xx restarts the flow.- Rendering: parse HTML into the DOM and CSS into the CSSOM; classic scripts block parsing unless
deferorasync; build the render tree, then layout, paint and compositing. Subresources are discovered during parsing and fetched in parallel over the reused connection. - Close by linking layers to latency: DNS, TCP and TLS each add round trips (mitigated by connection reuse,
preconnect, CDNs and TLS session resumption), and the critical rendering path decides how soon First Contentful Paint and LCP happen.
Follow-up
- What does HTTP/2 change over HTTP/1.1? One connection multiplexes many streams with binary framing and HPACK header compression, so domain sharding is unnecessary; TCP head-of-line blocking remains until HTTP/3.
- How does the browser decide the certificate is valid? It builds a chain from the leaf to a trusted root, checks each signature, the hostname against the SAN, validity dates, and revocation and CT requirements.
- What if DNS returns several IP addresses? Round-robin record ordering spreads load, the client falls back to the next address if one fails, and Happy Eyeballs races IPv6 against IPv4 so a broken path does not stall the connection.
What are the core principles of RESTful API design?
Easy · Technical Foundations and Domain Knowled
Approach
- REST is an architectural style defined by constraints (Fielding): client-server, statelessness, cacheability, a uniform interface, a layered system, and optional code-on-demand. Naming these, rather than 'JSON over HTTP', is the precise answer.
- Uniform interface in practice: resources are nouns with stable URIs (
/orders/42,/users/7/orders), manipulated through representations such as JSON, with self-descriptive messages and hypermedia links (HATEOAS), which Fielding requires but most practical APIs skip. - Use methods by their semantics: GET is safe and idempotent, PUT replaces and is idempotent, DELETE is idempotent, PATCH is a partial update, POST creates or triggers non-idempotent actions. Use status codes precisely: 201 with
Location, 204, 400, 401 vs 403, 404, 409, 422, 429. - Statelessness: every request carries what the server needs (auth token, parameters) and no client session lives on the server between calls, so any instance can serve any request, easing horizontal scaling and failover. Retry safety comes from idempotency, not statelessness.
- Cacheability and layering: responses declare
Cache-ControlandETagso browsers, CDNs and proxies can cache GETs; conditional requests (If-None-Matchreturning 304,If-Matchfor optimistic concurrency) save bandwidth and prevent lost updates. - Place an API on the Richardson maturity model: level 0 tunnels everything through one endpoint, level 1 adds resources, level 2 adds proper methods and status codes, level 3 adds hypermedia controls. It shows where an API falls short of REST instead of calling any CRUD-over-HTTP JSON API RESTful.
Follow-up
- Is PATCH idempotent? Not by definition; a merge patch that sets fields usually is, but an operation like 'append an item' is not, so clients should not blindly retry it.
- How would you version a REST API? A URI prefix (
/v1) is easiest to route and cache, while header or media-type versioning keeps URIs stable at the cost of harder testing and caching. - When would you choose GraphQL or gRPC instead? GraphQL when clients need flexible reads aggregated across many resources; gRPC for low-latency internal service calls with strict, generated schemas.
Can you explain the SOLID principles and their importance in software development?
Medium · Technical Foundations and Domain Knowled
Approach
- SOLID is five object-oriented design principles popularised by Robert C. Martin, all aimed at code that is cheap to change by limiting how far a change ripples. Define each precisely with a small example; reciting the acronym alone is the weak answer.
- Single Responsibility: a module should have one reason to change, meaning it answers to one actor. An
Invoiceclass that calculates totals, renders a PDF and saves to the database changes for three reasons; split it into calculator, renderer and repository. - Open/Closed: open for extension, closed for modification. Support a new payment method by adding a class that implements a
PaymentProviderinterface and registering it, instead of editing a growingswitchin code that is already tested. - Liskov Substitution: a subtype must work anywhere its base type is expected without surprising callers: no stronger preconditions, no weaker postconditions, no new exceptions.
Square extends Rectangleviolates it, because setting the width silently changes the height. - Interface Segregation: clients shouldn't depend on methods they don't use, so prefer small role interfaces. Dependency Inversion: high-level policy and low-level detail both depend on abstractions, e.g.
OrderServicetakes aRepositoryinterface, not a concrete database class. - Why they matter: lower coupling, units testable with fakes, safer extension by many people. Applied dogmatically they breed needless abstraction, so add an interface when a second implementation or a test seam actually needs it.
Follow-up
- Is dependency injection the same as dependency inversion? No; inversion is the principle about which way dependencies point, while injection is one technique for supplying the concrete implementation.
- How do these ideas apply in JavaScript or functional code without classes? Small single-purpose functions, passing functions or modules in as dependencies, and composition over inheritance carry the same intent.
- Which principle is most often violated in practice? Usually Single Responsibility, as classes accrete features; spot it when unrelated changes keep touching the same file and its tests need many unrelated mocks.
What is a microservices architecture, and what are its trade-offs?
Medium · System Design and Architecture
Approach
- Define it: an application built as small, independently deployable services, each owning one business capability and its own data, talking over the network via HTTP, gRPC or messaging. The contrast is a monolith: one deployable unit, typically one shared database.
- Benefits: each team deploys on its own cadence, only the hot service needs scaling, failures can be isolated if boundaries and timeouts are right, and each service can pick its own tech. They mainly solve an organisational problem: many teams on one codebase.
- Costs: every call becomes a network call that can fail or stall, so you need timeouts, retries with backoff, circuit breakers and idempotent handlers. Cross-service writes lose ACID transactions, so you rely on sagas, the outbox pattern and eventual consistency.
- Operational tax: service discovery, an API gateway, distributed tracing, central logs and metrics, contract tests between services, a pipeline per service and clear on-call ownership. Following one request across eight services is far harder than reading one stack trace.
- Boundaries decide success: split along business capabilities (bounded contexts), not technical layers or tables, or you get chatty calls. Name the failure mode: a distributed monolith, where services share a database or must deploy together, paying all the costs for none of the benefits.
- Give a judgement: start with a well-modularised monolith and extract a service when team autonomy, scaling or deploy frequency justifies it, using the strangler fig pattern. Microservices are not automatically faster or more scalable.
Follow-up
- How do you keep data consistent when one business action spans two services? Use a saga with compensating steps and publish events through a transactional outbox so the database write and the event cannot diverge.
- Synchronous calls or asynchronous messaging between services? Synchronous when the caller needs an answer now; events for decoupling and resilience, accepting eventual consistency and harder tracing.
- How would you migrate a monolith? Strangler fig: route one capability at a time through a gateway to a new service, move ownership of its data, and delete the old path once traffic has moved.
How do you approach optimizing a frontend application for performance?
Medium · System Design and Architecture
Approach
- Measure first and name the targets: Core Web Vitals at the 75th percentile (LCP within 2.5 s, INP within 200 ms, CLS within 0.1), using field data from real-user monitoring plus lab runs in Lighthouse and the DevTools Performance panel on throttled, mid-range devices.
- Ship less JavaScript: analyse the bundle, code-split by route, lazy-load below-the-fold components, tree-shake, and drop heavy dependencies. Byte for byte it is the costliest resource, since after download it must be parsed, compiled and executed, mostly on the main thread.
- Shorten the critical path for LCP: server-render or pre-render above-the-fold content, inline critical CSS,
deferscripts,preloadthe LCP image or font,preconnectto key origins, compress with Brotli, and serve hashed static assets from a CDN with long-livedCache-Control. - Images are usually the biggest bytes: use AVIF or WebP, responsive
srcsetandsizes,loading="lazy"for offscreen images (never the LCP image), and explicitwidth/heightoraspect-ratioso nothing shifts when they arrive. - Fix responsiveness (INP): find long tasks over 50 ms in a performance trace, split them or yield to the main thread, move heavy work to Web Workers, debounce input handlers, virtualise long lists, and cut needless re-renders with memoisation and stable props.
- Stop regressions: performance budgets in CI (bundle size, Lighthouse CI), per-page RUM dashboards and alerts. Report results as before-and-after numbers, e.g. 'LCP p75 from 4.1 s to 2.3 s after image and bundle work'.
Follow-up
- How would you debug a poor INP score? Record the slow interaction in DevTools, find the long task delaying input handling or the next paint, then split it, defer non-urgent updates or shrink the DOM.
- SSR or client-side rendering for performance? SSR improves first paint, LCP and crawlability but costs server time and hydration work; CSR is cheaper to host but slower to First Contentful Paint and LCP on weak devices.
- What causes layout shift and how do you fix it? Images or embeds without reserved space, web fonts swapping in, and injected banners; set dimensions, reserve slots, and use metric-matched fallback fonts.
What are the primary differences between using Docker and traditional Virtual Machines?
Easy · System Design and Architecture
Approach
- The core difference is what gets virtualised: a VM virtualises hardware through a hypervisor and runs a full guest OS with its own kernel, while a Docker container is an isolated process on the host kernel, fenced by Linux namespaces and limited by cgroups.
- Consequences: containers start in well under a second to a few seconds and images are often megabytes, so many fit on a host; VMs boot a whole OS and weigh gigabytes, but can run a different kernel or OS, such as a Windows guest on a Linux host.
- Isolation: a VM boundary is stronger because escaping a hypervisor is harder than exploiting a shared kernel. Harden containers with a non-root user, read-only filesystem, dropped capabilities and seccomp, or use sandboxed runtimes such as gVisor or Kata Containers for untrusted code.
- Packaging: a Docker image is a layered, immutable filesystem built from a
Dockerfile, and one image digest gives every environment identical contents; rebuilds match only if base-image digests and package versions are pinned. A VM image carries a whole OS and is usually patched in place. - They are complementary: in the cloud, containers usually run inside VMs, scheduled by an orchestrator such as Kubernetes, and Docker Desktop on macOS or Windows itself runs a lightweight Linux VM.
- Choose VMs for strong multi-tenant isolation, a different kernel, or legacy stateful systems; choose containers for stateless services, CI jobs and dense, fast-scaling workloads. Avoid calling a container a 'lightweight VM': it has no guest kernel.
Follow-up
- What happens to data when a container is removed? Its writable layer is discarded; persist state in volumes or bind mounts, or better, in external managed storage.
- Why keep images small? Faster pulls and cold starts and a smaller attack surface; use multi-stage builds and slim or distroless base images.
- What happens when a container exceeds its memory limit? The cgroup limit is enforced and the kernel OOM-kills the process, which usually surfaces as exit code 137.
How would you design a RESTful API for a specific, given task?
Medium · System Design and Architecture
Approach
- Treat it as a small design interview: restate the task, list actors and use cases, and agree on non-functional needs (read/write ratio, auth, volume) before naming endpoints. The steps below use an example domain, e.g. a room-booking service: create, view, list and cancel bookings.
- Model the data first and derive resources from the nouns:
Room {id, name, capacity},Booking {id, roomId, userId, start, end, status}. Relationships decide nesting:/rooms/{id}/bookingsfor scoped listing, top-level/bookings/{id}for direct access. - Map operations to methods and codes:
POST /bookingsreturns 201 withLocation;GET /bookings/{id}returns 200 or 404;PATCHfor partial edits; cancel viaDELETEorPOST /bookings/{id}/cancelif it is a state change with side effects. Return 409 on a double-booking. - Design collections deliberately: cursor pagination (
?limit=20&cursor=...returningnextCursor), filters and sort as query parameters (?roomId=3&from=...), and one error body shape such as{code, message, details}used consistently across endpoints. - Make it correct under retries and concurrency: accept an
Idempotency-Keyon POST so retried requests don't create duplicates, useETagwithIf-Matchfor optimistic locking on updates, and enforce invariants such as no overlapping bookings in the database, not just in application code. - Finish with cross-cutting concerns: bearer-token auth plus per-resource authorisation, rate limiting (429 with
Retry-After), a versioning policy, caching headers on GETs, and an OpenAPI spec so clients, docs and contract tests share one definition.
Follow-up
- How do you stop two users booking the same slot at once? Enforce it in the database: a PostgreSQL exclusion constraint on room and time range, or lock the room row with
SELECT ... FOR UPDATEbefore the overlap check; map violations to 409. - Offset or cursor pagination? Offset is simple but slow on deep pages and skips or repeats rows when data changes; a cursor over an indexed, unique sort key is stable and fast.
- How do you evolve the API without breaking clients? Make additive changes only, never repurpose a field, announce deprecations with a timeline, and bump the version only for genuinely breaking changes.
How would you approach writing unit tests for your implementation?
Easy · Coding and Problem-Solving
Approach
- Test behaviour, not implementation: list what the unit promises (outputs for inputs, side effects, errors) and test through its public interface so refactoring doesn't break tests. Name tests as behaviours, e.g.
test_returns_empty_list_when_nothing_matches. - Enumerate cases systematically: the happy path, boundaries (empty, single element, zero, negative, maximum size), invalid input and error paths, and every branch. Equivalence partitioning plus boundary values gives strong coverage with few tests.
- Structure each test as Arrange-Act-Assert with one behaviour per test, and use parameterised tests (
pytest.mark.parametrize, Jestit.each) for tables of input and expected output instead of copy-pasted tests. - Isolate the unit: inject dependencies so databases, HTTP clients, clocks and random generators can be swapped for fakes or stubs. Mock only at boundaries; mocking internals couples tests to the implementation and makes them break on every refactor.
- Keep tests fast, deterministic and independent: no real network or
sleep, fixed time and seeds, no shared mutable state between tests. A flaky test is worse than none because the team learns to ignore red builds. - Treat coverage as a gap finder, not a target, since 100% line coverage can still miss cases. Mention property-based testing (e.g. Hypothesis) or mutation testing for logic-heavy code, and in a live session write a failing test first so intent is visible.
Follow-up
- What's the difference between a stub and a mock? A stub returns canned data so the code can run; a mock also verifies interactions, such as
send_emailbeing called once with given arguments. - How do you test code that depends on the current time? Inject a clock or pass
nowas a parameter, or freeze time in the test, so the result never depends on when the test runs. - Where do unit tests end and integration tests begin? Unit tests exercise one module with dependencies replaced; integration tests use real collaborators such as a database, catching wiring and query bugs that fakes hide.
Can you walk me through your approach to debugging a piece of broken code?
Easy · Coding and Problem-Solving
Approach
- Reproduce first: get the exact input, environment and error, and shrink it to the smallest reliable repro, ideally a failing test. A bug you cannot trigger on demand is a bug you cannot prove fixed.
- Read the evidence before editing anything: the full error message and stack trace (the first frame in your own code is usually the place to start), logs around the failure, and what changed recently: commits, dependency bumps, config, data.
- Form one hypothesis at a time and test it cheaply: inspect actual versus expected state at key points with a breakpoint or targeted log, and halve the search space by bisecting inputs, disabling half the code path, or running
git bisectbetween a good and a bad commit. - Check the likely suspects for the symptom: off-by-one and boundary cases, null or undefined values and type coercion, shared mutable state, async ordering or a missing
await, a wrong assumption about a library's behaviour, and environment differences such as versions, timezone or config. - Fix the root cause, not the symptom: explain why it broke, make the smallest change that addresses it, keep the failing test as a regression test, run the wider suite, and search for the same pattern elsewhere.
- Handed a snippet with no debugger, trace it by hand: pick a small failing input, write down each variable's value line by line next to what that line should produce, and the first line where the two diverge is where the bug is.
Follow-up
- What if the bug only happens in production? Pull the logs, traces and failing request, reproduce with production-like data and config, and add targeted logging behind a flag rather than debugging live.
- How do you debug an intermittent failure? Suspect races, timing, shared state or external dependencies; run it in a loop, add timestamps and correlation IDs, and control time and randomness to make it deterministic.
- When is
git bisectthe right tool? When something used to work and the cause is unclear; it binary-searches commits, andgit bisect runautomates it with a test script.
Suggested practice checklist
Built from the rounds and topics Gumtree candidates report.
Day 1 — Map the Gumtree loop
- Write out the reported sequence: Recruiter Screen, Technical Component, On-site/Virtual Interview.
- For each round, write one sentence on what it is judging, from the description above, and mark the one you are least ready for.
Deliverable: A one-page map of the 3 reported rounds, with the weakest marked.
Day 2 — Work Spring Boot
- Spend the session on Spring Boot, which Gumtree candidates report being tested on.
- Write one worked example in Spring Boot and time yourself on it.
Deliverable: One timed worked example in Spring Boot.
Day 3 — Work Hibernate (ORM)
- Spend the session on Hibernate (ORM), which Gumtree candidates report being tested on.
- Write one worked example in Hibernate (ORM) and time yourself on it.
Deliverable: One timed worked example in Hibernate (ORM).
Day 4 — Work RESTful APIs
- Spend the session on RESTful APIs, which Gumtree candidates report being tested on.
- Write one worked example in RESTful APIs and time yourself on it.
Deliverable: One timed worked example in RESTful APIs.
Day 5 — Answer out loud: Technical Foundations and Domain Knowledge
- Answer aloud, timed: How does JavaScript manage memory, and how would you detect or fix a memory leak?
- Answer aloud, timed: Can you explain the event loop and the concept of closures?
Deliverable: Spoken answers to 2 reported Technical Foundations and Domain Knowledge question(s), under time.
Day 6 — Answer out loud: System Design and Architecture
- Answer aloud, timed: What is a microservices architecture, and what are its trade-offs?
- Answer aloud, timed: How do you approach optimizing a frontend application for performance?
Deliverable: Spoken answers to 2 reported System Design and Architecture question(s), under time.
Day 7 — Answer out loud: Coding and Problem-Solving
- Answer aloud, timed: Can you walk me through your approach to debugging a piece of broken code?
- Answer aloud, timed: How would you approach writing unit tests for your implementation?
Deliverable: Spoken answers to 2 reported Coding and Problem-Solving question(s), under time.
Behavioural preparation
Behavioural rounds judge the decision you made and what it cost.
Unblock an engineer without taking the keyboard
Easy · mentoring · leases · at-least-once
A teammate has spent two days on a job handler that occasionally writes duplicate rows. They are certain the queue is delivering twice by mistake. You suspect a lease expiring under a slow handler, so the job is running concurrently with itself. Describe how you have unblocked someone in this position: what you asked before offering a hypothesis, what you showed them rather than told them, and what you left them owning. Then say what you would do if their theory turned out to be the right one.
Approach
- Ask before diagnosing, and ask for things answerable from data they already have: the attempt count on the job rows that produced duplicates, the handler's observed duration against its lease expiry, and whether the duplicate rows share a natural key that a unique constraint could have caught.
- Teach the shape rather than the answer. A lease cannot distinguish a dead worker from a slow one, so a handler that outruns its lease is running twice by design, and deploys deliver the other half by killing handlers mid-run on every rollout. Both of their candidate theories produce identical duplicate rows, which is why the evidence has to come from timings rather than from argument.
- Hand over a checklist they execute: a natural key on every write the handler performs so the second copy collides rather than appends, the record of intent written before any external effect, a lease heartbeat while running, and the metric that shows it working.
- Keep ownership with them deliberately. Pair on the first write, then step back; if you finish it yourself you have closed one ticket and left the same person stuck on the next redelivery.
Follow-up
- How would you distinguish a genuine double-delivery from a lease expiry using only the data already stored?
- Their handler calls an external endpoint before recording that it did. What do you tell them to change first?
Argue against a design, lose, and commit anyway
Medium · disagreement · service boundaries · decision records
Describe a design you argued against and lost. State the failure you predicted as a named mechanism, not a feeling about complexity: two services that would need one transaction, a projection with no rebuild path, a write path with no idempotency key. Say what evidence you brought, what the decision maker weighed instead, and what you did after the decision was made: what you instrumented, what you wrote down, and whether the prediction came true. Five minutes.
Approach
- State the prediction in falsifiable form up front: the mechanism, the condition that triggers it, and the observable outcome. A prediction that cannot be checked also cannot be credited to you later.
- Show the evidence you had at the time and label each piece honestly as measured, analogous, or intuition. Keeping the intuition is fine; disguising it as data is the thing that erodes your standing in the next argument.
- Represent the opposing case at full strength, including the constraint you did not control: a fixed date, a team boundary, or the fact that the decision was cheap to reverse and yours was not.
- Make disagree-and-commit concrete. Name the artefact you left behind so the prediction could be settled without you: the alert and its threshold, the counter on the dashboard, the decision note that recorded the trade-off and the condition that would revisit it.
Follow-up
- What threshold on that alert would have proved you right, and did anyone ever look at it?
- If the same proposal arrived tomorrow with the same deadline, would you argue it the same way?
Narrate an outage you owned from page to postmortem
Hard · incident response · blast radius · postmortems
Pick an incident you personally drove, ideally one where writes were affected rather than reads. In six to eight minutes: state the symptom as it first appeared on a dashboard, the blast radius you established before you knew the cause, the mitigation you applied and when, the mechanism you eventually proved, and the follow-up that would prevent a repeat. Bring numbers: error rate, tenants affected, minutes to mitigate, minutes to resolve. If you cannot name what you measured, choose a different incident.
Approach
- Open on the signal rather than the cause: which metric at which percentile moved, on which service, at what time, so the listener follows the same evidence you had rather than a conclusion you already reached.
- Separate mitigation from diagnosis out loud. State what you did to stop the bleeding (flag off, shed traffic, drain a lease, roll back a deploy) and say plainly that you did it before the mechanism was known, because those are two jobs with different deadlines.
- Establish blast radius in countable terms: how many tenants, how many writes, and crucially whether the effect was loss or only delay. An append-only revision table or a pending outbox row means the change survived and the projection was merely behind, which is a repair rather than a data-loss incident.
- Prove the mechanism instead of asserting it. Name the trace span that grew, the plan that flipped to a sequential scan, the lease that expired, plus one alternative you ruled out and the signal that stayed flat while you ruled it out.
Follow-up
- What would you do differently in the first five minutes, given the same dashboard and no more information?
- Which follow-up action did you deliberately not take, and why was dropping it the right call?