A Software Engineer at Revolut builds backend systems for banking, trading, crypto and payment products. Candidate reports put the work in core banking features, risk and fraud detection pipelines, distributed ledgers and payment gateways. The reported stack is Java, Kotlin and Scala backends on transactional databases such as PostgreSQL. The reported questions focus on thread safety, ledger correctness and data isolation.
The reported technical questions lean toward practical backend problems rather than abstract puzzles. Coding examples include an in-memory banking service that rejects invalid transactions, a thread-safe money transfer, and a load balancer with Random and Round Robin selection. The reported database questions go deep on PostgreSQL isolation levels, optimistic versus pessimistic locking, and index choice. The reported design questions are about banking systems: booking against unreliable third-party APIs, distributed transactions, and reliable event publishing.
Reported rounds expect working code, tests and modular design, not pseudocode. Aim your preparation there: finished, tested solutions to money-movement problems, and theory answers that name the exact anomaly or failure a mechanism prevents.
Initial Screening
reportedThis stage is only described as an initial screening to assess candidate fit, so the reports do not say what it covers. Prepare for the two things a screen can end on. The first is logistics that surface late: start date, notice period, location and work authorisation. The second is a clear, short account of your backend experience. The later stages are described as live coding, theory questions on databases and concurrency, and banking-domain design, so use this call to confirm which language you will code in and what the live coding environment looks like. The hands-on stage is described as coding in your own IDE, and knowing that early leaves time to get it ready.
What to demonstrate
- Whether your background and the role's backend focus line up. Be ready to name the languages, databases and concurrency work you have actually shipped.
- Whether hard constraints such as start date, notice period, location and authorisation are compatible before technical rounds are scheduled
How to prepare
- Write a short summary of one backend system you built where correctness under concurrency or failure mattered. Cover what it did, what could go wrong, and what you did to prevent it.
- Ask the recruiter which language you can use for live coding, whether you code in your own IDE, and whether tests are expected. Write down the answers.
- List your constraints and your compensation range as one-line facts before the call, so you are not working them out live.
Hands-on Technical Validation
reportedThis stage is described as live coding on real-world backend scenarios, writing code in your own IDE. The sources do not say which questions are asked here, but the reported coding-category questions are the right practice set for it: an in-memory banking service with deposits, withdrawals and transfers that rejects invalid transactions atomically, a Minesweeper-style board generator, a custom Set built without Java collections, hash tables or trees, and a load balancer with Random and Round Robin selection. The source's tip for live rounds is complete, working code with tests (TDD) and modular design, not pseudocode. If the interviewer asks whether you want to add more validations or tests, treat it as a prompt to go looking for edge cases. Avoid two time sinks: coding before a failing test exists, and making random fixes to a failing case you have not isolated.
What to demonstrate
- Whether the code runs and is covered by unit tests you wrote as you went, not added at the end
- Whether the design stays modular, with the domain (accounts, transactions, balances) kept apart from input handling
- Whether invalid inputs are rejected explicitly and atomically, such as a negative amount, overdraft, unknown account or transfer to the same account
- Whether randomness is testable, such as a seeded or injected random source so the output is deterministic under test
How to prepare
- Before the round, configure your IDE with a test runner and a project template that compiles and runs one passing test, so setup costs you nothing
- Build the in-memory banking service test-first: write a failing test for each rejection rule before the code that enforces it, and keep amounts in BigDecimal or integer minor units
- For the mine grid, inject the random source and assert invariants (exactly k mines, every count equals its mined neighbours) rather than exact layouts. Place k mines with selection sampling so no extra array is needed.
- Rehearse a thread-safe Round Robin selector using an AtomicInteger counter and Math.floorMod, so the index stays valid after the counter overflows
Theory Questions
reportedThis stage is described as targeted theory questions on databases and concurrency. The sources do not say which questions belong to it, so prepare from the two reported categories that match. The reported database questions cover PostgreSQL isolation levels (Read Committed, Repeatable Read, Serializable) and the anomalies each allows, optimistic versus pessimistic locking, B-Tree versus BRIN indexes on high-volume ledger tables, and sharding, partitioning and replication; the source also lists MVCC as an area to be ready on. The reported concurrency questions cover Java thread safety beyond global synchronized blocks, race conditions on shared caches, and deadlock-free transfers, and each can be answered as theory as well as code. Short answers that name a mechanism without its failure mode do not hold up to follow-up questions. For each mechanism, say what it prevents, what it still allows, and what it costs.
What to demonstrate
- Whether you can map each PostgreSQL isolation level to the anomalies it prevents and allows, including that its Repeatable Read is snapshot isolation that still permits write skew, and that Serializable needs a retry on SQLSTATE 40001
- Whether you can say when SELECT ... FOR UPDATE beats a version-column optimistic check, and what each does under high contention
- Whether you know the JVM primitives (synchronized, ReentrantLock, ReadWriteLock, atomic variables, concurrent collections) and when each is the cheapest correct choice
- Whether index answers are tied to access patterns, for example BRIN relies on physical order matching the column, such as an append-only timestamp
How to prepare
- Build a grid with isolation levels as rows and anomalies (dirty read, non-repeatable read, phantom, serialization anomaly) as columns, fill it in for PostgreSQL, and explain each cell aloud
- Reproduce a lost update in two psql sessions under Read Committed with SELECT then UPDATE, then fix it three ways: an atomic conditional UPDATE, SELECT ... FOR UPDATE, and a version column
- Write a two-account transfer that deadlocks with naive locking, then fix it by locking in ascending account id order. Be ready to explain tryLock with a timeout as the alternative.
- Explain B-Tree versus BRIN for a ledger table in three sentences: size, which queries each serves, and why BRIN cannot back a uniqueness constraint
System Design Round
reportedThis round is described as focused on real-world banking domain challenges rather than abstract concepts. The sources do not tie specific questions to it, but the reported design-category questions are the practice set: an apartment booking system that integrates with unreliable third-party APIs without double bookings, a URL shortener with distributed caching and high availability, a card ordering feature, a high-throughput ledger with strict ordering and idempotency, multi-region banking data synchronisation, and conceptual questions on SAGA versus Two-Phase Commit, the Transactional Outbox with Kafka, and DDD, CQRS and Event Sourcing for a ledger. The same themes recur: every external call can fail or repeat, so correctness has to come from idempotency keys, local transactions and compensation rather than hoping the call succeeds once.
What to demonstrate
- Whether you pin down the invariant first, for example no double booking or no double spend, and show where in the design it is enforced
- Whether failures of third-party calls are handled explicitly: timeouts, retries with idempotency keys, compensating actions and reconciliation
- Whether you can compare SAGA and 2PC, and explain why an outbox row written in the same local transaction avoids the dual-write problem
- Whether trade-offs are stated and justified, for example consistency versus availability across regions or read models split from write models under CQRS
How to prepare
- Walk the apartment booking design end to end: local reservation hold, idempotent call to the partner API, confirmation or compensation, and a reconciliation job for unknown outcomes
- Explain the Transactional Outbox from write to consumer: business row and outbox row in one transaction, a relay that publishes to Kafka, at-least-once delivery, consumer deduplication by event id
- Compare an orchestrated saga with 2PC on the same transfer, covering who holds locks, what happens when the coordinator dies, and what a compensation looks like
- Work through the drill 'Raise the write ceiling on one hot clearing account' to practise stating a throughput ceiling as a number before proposing a design
18 candidate reports. Individual accounts describe a particular role and hiring cycle.
Revolut Product Manager interview: hypothetical business case
My process felt lighter than the intense multi-round processes I had seen elsewhere. It started with an HR screen and moved into a case-style evaluation. Recruiter interactions were straightforward, prep materials were provided, and I was expected to do structured analysis. For the case, I worked through a business scenario and gave reasoning and a recommendation. The examples were hypothetical r…
Read full experienceRevolut Operations Manager Interview Experience: a structured process with late red flags
My process was long and structured: a recruiter screen, a cognitive-style test, a home task, a case interview, a bar raiser, and a team-fit stage. The recruiter did a fair amount of preparation between rounds and kept things moving, although there were hiccups. Interviews could start more than 10 minutes late without much notice. The bar raiser was much shorter than I expected. Parts of the proce…
Read full experienceRevolut Software Engineer interview: distributed systems and opaque system design
The process started smoothly with a recruiter screen and technical rounds rooted in practical distributed-systems patterns. I cleared several early stages, but I did not get through the system design interview. That round was difficult because I could not tell what the interviewer wanted me to emphasize or how quickly to move through the discussion. The earlier coding and technical work was concr…
Read full experienceRevolut Software Engineer concurrency and database interview experience
The interviews centered on backend fundamentals: concurrency, databases, and how transactions behave under pressure. The process began with an HR round that included technical scenarios rather than staying purely behavioral. I was asked about concurrency and isolation, and we discussed database topics such as locking, CQRS, sharding, and indexes. Later technical interviews focused on production-r…
Read full experienceRevolut Operations Manager Interview Experience: take-home case rejected before deadline
The process had a recruiter screen, an online assessment, a live case interview, and then a week-long take-home business case. The assignment felt like substantial unpaid work: it involved a JIRA workflow, process maps, SQL queries, and a full fraud-detection analysis. I was rejected while I was still finishing the take-home, before its deadline. That made the process feel disorganized or dishone…
Read full experiencePracHub editorial advice for the preparation topics above.
Writing the implementation first and adding tests at the end, or not at all
Reported live rounds expect test coverage written TDD-style. Start with one failing test for the simplest rule, make it pass, then add a failing test for each rejection rule (negative amount, overdraft, unknown account, self-transfer) before the code that enforces it. For random output, inject a seeded source and assert invariants rather than exact values.
Naming an isolation level without the anomalies it still allows
PostgreSQL defaults to Read Committed, where every statement takes a fresh snapshot, so a SELECT-then-UPDATE on a balance loses updates under concurrency. Its Repeatable Read is snapshot isolation: it aborts the conflicting writer with SQLSTATE 40001 but still permits write skew across two rows, which only Serializable closes. Both stronger levels need a bounded retry loop on 40001. Say which anomaly each level stops and which it does not.
Locking the two accounts of a transfer in call order
Transfers A to B and B to A running together each hold one lock and wait for the other. Always acquire locks in a fixed order, such as ascending account id, in Java code and in SQL alike (on the database side, PostgreSQL reports the deadlock as SQLSTATE 40P01). Mention tryLock with a timeout as the alternative and what it costs: retries and possible starvation.
Holding money in double or a hard-coded two-decimal format
Binary floating point cannot represent 0.1 exactly, so repeated arithmetic drifts and equality checks fail. Use BigDecimal with an explicit scale and rounding mode, or integer minor units with the currency's exponent carried alongside it, since ISO 4217 exponents are 0 for JPY, 2 for most currencies and 3 for BHD or KWD. State the one place rounding happens.
Designing as if a third-party or cross-service call happens exactly once
In the booking, ledger and outbox questions, assume every external call can time out with an unknown result and every message can arrive twice. Show the idempotency key, the local transaction that records intent, the compensation path, and the reconciliation job that resolves unknown outcomes. Name where the no-double-booking or no-double-spend invariant is enforced.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
How do you structure unit tests using TDD to test functions that gener…
How do you structure unit tests using TDD to test functions that generate non-deterministic or random output?
Approach
- Choose the data structure from the access pattern, not from familiarity.
- State the target complexity and say which constraint rules the naive version out.
- Name the brute-force solution and its complexity before improving on it.
Follow-up
- Which test case would catch an off-by-one here?
- How does this change if the input no longer fits in memory?
How do you choose between using primitive data types versus arbitrary-…
How do you choose between using primitive data types versus arbitrary-precision types like BigDecimal when handling multi-currency balances?
Approach
- Name the brute-force solution and its complexity before improving on it.
- State the target complexity and say which constraint rules the naive version out.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- How does this change if the input no longer fits in memory?
- What is the worst case, and how likely is it on real data?
Write a program to generate a grid matrix filled with randomly placed …
Write a program to generate a grid matrix filled with randomly placed mines while maintaining an optimal space complexity.
Approach
- State the target complexity and say which constraint rules the naive version out.
- Walk one small example through your approach before writing the whole thing.
- Restate the input: its shape, its size, and what is guaranteed about it.
Follow-up
- Which test case would catch an off-by-one here?
- What is the worst case, and how likely is it on real data?
What mechanisms would you use to handle race conditions when writing t…
What mechanisms would you use to handle race conditions when writing to a shared cache or state across multiple worker threads?
Approach
- Name what is shared across threads and what owns each piece of state.
- Say what the runtime actually does before reasoning about the code.
- Reach for the cheapest primitive that closes the race, not the broadest lock.
Follow-up
- How would you prove the race exists rather than suspect it?
- Where could this allocate more than you expect?
How do you ensure thread safety in Java without relying entirely on gl…
How do you ensure thread safety in Java without relying entirely on global synchronized blocks?
Approach
- Say what the runtime actually does before reasoning about the code.
- Reach for the cheapest primitive that closes the race, not the broadest lock.
- Identify the window where an invariant is briefly untrue.
Follow-up
- Where could this allocate more than you expect?
- What happens if two callers reach this at the same time?
Compute peak held exposure from overlapping authorisation holds
An account has up to 2 million authorisations on one business date: (auth_id, amount_minor, created_at, expires_at) with the hold live over the half-open interval [created_at, expires_at), plus capture events (auth_id, captured_minor, captured_at) that reduce the hold at captured_at, and explicit reversals that drop the remainder to zero. Timestamps are microsecond-precision timestamptz. Return the maximum total held amount across the day and the earliest instant it is reached, with complexity. Then say what changes if the deliverable is the peak per minute instead.
Approach
- Expand each authorisation into signed delta events rather than reasoning about intervals:
+amountatcreated_at,-remainingatexpires_at,-captured_minorat eachcaptured_at,-remainingatreversed_at. The problem collapses to a running sum over a sorted event list. - Sort the 2n to 4n events by
(timestamp, sign)with negative deltas ordered first on a tie. The half-open convention forces that: at exactlyexpires_atthe hold is already gone, so a-must land before a+at the same instant or you report a one-microsecond peak that never existed. O(n log n) time, O(n) space. - Sweep once, tracking
running,bestandbest_at, taking the first instant that attains the maximum. Say out loud which tie rule you are using — 'the peak' is ambiguous when the same level is reached twice, and the caller needs to know which instant they are being handed. - If timestamps are bucketed (1,440 minute buckets for the per-minute variant), drop the sort for a difference array: add the delta at the start bucket, subtract at the end bucket, prefix-sum once. O(n + B) time and O(B) space, strictly better, at the cost of answering only at bucket resolution.
- Assert the invariant during the sweep:
runningmust never go negative. A negative total means a capture exceeded its authorisation, which is an invariant violation upstream rather than a sweep bug — fail loudly instead of clamping at zero and reporting a plausible number. - Handle carry-in: a hold created before the window contributes its remaining amount as the sweep's initial value, not as a
+event inside the window. Omitting that is the off-by-a-day that makes the first minute of every day look artificially quiet.
Worked solution 25 min
- Write the event expansion and the comparator first; the comparator is the part that is wrong in most first attempts.
- Fixture A: two holds of 10,000 minor units where the first's
expires_atequals the second'screated_at. - Fixture B: one hold of 10,000 with a partial capture of 4,000 at t+1 and expiry at t+2, so the held series is 10,000 then 6,000 then 0.
- Fixture C: a hold opened the previous day and still live at the window start; seed
runningwith its remaining amount. - Shuffle the events in all three fixtures before sorting and re-run, proving the answer depends only on the comparator.
Follow-up
- An incremental authorisation raises an existing hold after the fact. Where does that event go, and does it disturb the tie rule?
- You now need the peak for 10 million accounts inside a nightly window. What changes, and what must the partitioning key be?
- The peak sizes a funding transfer. Does the business date or the timestamp decide which day that transfer lands on?
Version a loan schedule instead of soft-deleting posted instalments
loan_instalment is keyed by (loan_id, schedule_version, instalment_no) and carries due_date, principal_minor, interest_minor, fee_minor, paid_principal_minor, paid_interest_minor, status, days_past_due, effective_from (date) and superseded_at (timestamptz). A borrower defers two payments on 2026-03-14, and instalments 1 to 6 already have allocations posted against them. Model exactly what the deferral writes, and write the query that returns the schedule as the borrower saw it on an arbitrary date. Say why stamping the old rows with deleted_at, or updating them in place, fails an audit.
Approach
- Treat the deferral as an insert, not an edit: write a complete new schedule_version with effective_from = the deferral instant on 2026-03-14, and in the same transaction stamp superseded_at on every row of the outgoing version with that identical instant, so the two version windows are half-open and adjacent rather than overlapping. Instalments 1 to 6 are reproduced unchanged, because they are what the borrower was told and what was posted to the ledger.
- Write the as-of read as a version selection, not a row filter: WHERE loan_id = $1 AND effective_from <= $2 AND (superseded_at IS NULL OR superseded_at > $2), then assert that exactly one schedule_version comes back - COUNT(DISTINCT schedule_version) = 1, not one row - so an overlapping window raises rather than silently returning two interleaved schedules under one instalment_no.
- Fix the type mismatch before writing that predicate: effective_from is declared date and superseded_at timestamptz, so comparing them casts the date at the session TimeZone and two readers in different zones select different versions near midnight. Put both columns in one domain, timestamptz, keep the window half-open with effective_from inclusive and superseded_at exclusive, and resolve a bare as-of date to an instant once, in the loan's booking timezone, at the edge of the system.
- Say what deleted_at loses. It records that a row stopped being current but not what replaced it or from when, it leaves every downstream query obliged to remember deleted_at IS NULL, and one query that forgets double-counts the schedule. Versioning puts the same information in the primary key where it cannot be forgotten.
- Close the arithmetic: under the loan's stated day-count convention the new version must still sum to outstanding principal plus scheduled interest to the minor unit, with the per-period rounding residual placed in one named instalment, conventionally the last, rather than smeared across the tail.
- Index (loan_id, effective_from DESC) for the as-of lookup, keep superseded versions online rather than archiving them, and enforce with a trigger that no UPDATE touches a row whose paid_principal_minor or paid_interest_minor is non-zero.
Follow-up
- What does days_past_due mean for an instalment that exists in two versions with different due_dates?
- A payment arrives allocated to an instalment_no that exists only in the superseded version. What do you do with it?
- How do you prove the ledger postings made under version 1 still reconcile once version 2 exists?
Make a charge endpoint safe under concurrent duplicate retries
idempotency_key holds id, scope, key, request_fingerprint (SHA-256 over the canonicalised body), status (in_progress, completed, failed), response_status, response_body, locked_at, completed_at, expires_at, created_at. Fifty identical create-payment requests carrying the same scope and key reach four application instances inside the same 20 ms. Give the DDL constraint and the exact statements the handler runs so that exactly one payment_intent is created and all fifty callers receive the same response body. State what you return when that key arrives with a different fingerprint, and what an arrival after expires_at means.
Approach
- Put the concurrency control in the schema: UNIQUE (scope, key). A SELECT-then-INSERT cannot work because both transactions can read nothing before either commits, so the check passes twice and the constraint then surfaces as an error on a payment that succeeded.
- Claim the key with INSERT ... ON CONFLICT (scope, key) DO NOTHING RETURNING id. A conflict returns zero rows rather than the existing row, so branch on rowcount: the winner proceeds, the loser reads the stored row.
- Keep that path on READ COMMITTED deliberately. The loser's follow-up SELECT takes a fresh statement snapshot and therefore sees the winner's committed row; under REPEATABLE READ the transaction snapshot predates that commit, the row stays invisible and the loser concludes the key does not exist.
- Split the work across two transactions because the processor call cannot sit inside one: commit the in_progress row with locked_at first so losers can see a claim, perform the effect, then write payment_intent plus status=completed with response_status and response_body in a single second transaction.
- Handle the crash window explicitly: a row stuck in_progress past its lease is an unknown outcome, not a failure, so the reaper queries the processor for that key before deciding. A loser that sees in_progress returns 409 and retries rather than repeating the effect.
- Compare request_fingerprint before replaying anything. Same key with a different body is 409, never the cached response, because replaying confirms a payment the caller did not request; and set expires_at beyond the client's and the processor's maximum retry horizon, since a replay after it is a genuinely new request.
Worked solution 20 min
- Create idempotency_key with UNIQUE (scope, key) and write the claim statement as INSERT ... ON CONFLICT DO NOTHING RETURNING id, plus the fallback SELECT on zero rows.
- Drive it with 50 threads sending the identical body, and assert one payment_intent row and fifty byte-identical response bodies.
- Re-send the same key with amount_minor changed by one unit and assert 409 plus no new payment_intent row.
- Kill the process between the processor call and the second commit, restart, and show the reaper resolves the in_progress row by querying the processor rather than by re-charging.
Follow-up
- The handler dies after the processor call and before the local commit. What does the next retry with that key observe, and how does the system converge on exactly one charge?
- Does the downstream processor honour an idempotency key of its own? Who mints it, and what breaks if a fresh one is generated per attempt?
- How do you purge rows past expires_at without the delete contending with the insert path?
How do the SAGA and Two-Phase Commit (2PC) patterns handle distributed…
How do the SAGA and Two-Phase Commit (2PC) patterns handle distributed transactions across microservices?
Approach
- State the consistency you need, and where you are willing to be stale.
- Name the read and write paths separately; they rarely have the same bottleneck.
- Fix the scope first: who calls this, how often, and what they do when it fails.
Follow-up
- How does this behave when that dependency is down for an hour?
- What would you drop to keep the system up under load?
Design an apartment booking system that integrates with unreliable thi…
Design an apartment booking system that integrates with unreliable third-party APIs while preventing double bookings.
Approach
- Name the read and write paths separately; they rarely have the same bottleneck.
- State the consistency you need, and where you are willing to be stale.
- Fix the scope first: who calls this, how often, and what they do when it fails.
Follow-up
- How does this behave when that dependency is down for an hour?
- What breaks first when traffic grows ten times?
How does the Transactional Outbox pattern guarantee message delivery t…
How does the Transactional Outbox pattern guarantee message delivery to an event bus like Kafka?
Approach
- Name the read and write paths separately; they rarely have the same bottleneck.
- Choose a partition key and say what query it makes expensive.
- Fix the scope first: who calls this, how often, and what they do when it fails.
Follow-up
- What would you drop to keep the system up under load?
- How does this behave when that dependency is down for an hour?
What are PostgreSQL transaction isolation levels, and how does Read Co…
What are PostgreSQL transaction isolation levels, and how does Read Committed differ from Repeatable Read and Serializable?
Approach
- Work from the requirement backwards to the design.
- Say what you would check first and why it is the highest-information step.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Raise the write ceiling on one hot clearing account
The ledger posts balanced entry sets at 10,000 entries/s into append-only ledger_entry(entry_id, transaction_id, account_id, direction, amount_minor>0, currency, source_type, source_id, business_date, posted_at), roughly three entries per transaction. A materialised row per account carries the floor check available_minor >= floor_minor and is updated in the posting transaction. One pooled clearing account appears in 40 percent of transactions. Compute the throughput ceiling on that one row, then raise it. Name the isolation level each version requires and state precisely what sharding the balance costs the floor check.
Approach
- Compute the ceiling before designing anything. Throughput against a single row is one divided by the lock hold time, where the hold runs from first acquiring the row to commit, including the WAL flush. At a 2 ms hold that is about 500 commits/s on that row, and adding cores does not move it. Demand is 10,000 entries/s at three entries per transaction, so roughly 3,300 transactions/s, of which 40 percent touch this account: about 1,300/s against a 500/s ceiling. You are 2.6x over before any code exists.
- Shorten the hold before sharding, because it is free. Acquire the hot row last in the transaction and hold it for exactly one statement: UPDATE balance SET available_minor = available_minor - $1 WHERE account_id = $2 AND available_minor - $1 >= floor_minor. This is correct at READ COMMITTED, because an UPDATE that blocks on a concurrent writer re-evaluates its WHERE clause against the newly committed row version rather than its original snapshot.
- Name what the broken version needs. SELECT the balance, compute in application code, then UPDATE loses updates at READ COMMITTED, since the second statement takes a fresh snapshot. Making that shape safe requires REPEATABLE READ or SERIALIZABLE plus a bounded retry loop on SQLSTATE 40001, which is the part most implementations omit; SELECT ... FOR UPDATE also works but is simply the same lock held for longer. Note that the identical application code changes behaviour on a different engine: InnoDB's REPEATABLE READ does not abort the loser.
- Order locks deterministically on any two-account movement, always ascending by account_id. Concurrent opposing transfers without that ordering deadlock with SQLSTATE 40P01, and the deadlock rate rises with exactly the concurrency you added to gain throughput.
- Shard the hot balance into N sub-rows keyed (account_id, shard_no), writers choosing a shard by hash of transaction_id. Per-shard hold time is unchanged, so capacity rises roughly N-fold, and at N=16 you clear 1,300/s with headroom. The cost is precise: the floor is now a predicate over the sum of N rows and is no longer expressible as a single-row CHECK, so it must be paid for deliberately. Either give each shard its own floor, which rejects postings while other shards hold headroom, or add a slow path that locks all shards in ascending shard_no under SERIALIZABLE, rebalances and retries on 40001. For a clearing account whose floor is effectively unreachable, dropping the per-shard floor and enforcing it as a monitored invariant is a defensible stated choice; for a customer deposit account it is not.
- Keep the entries immutable throughout: no UPDATE or DELETE grant on ledger_entry plus a trigger, corrections as reversing transactions, and partitioning by business_date so index writes stay in the current partition and retention is a DETACH.
Worked solution 45 min
- Run two concurrent sessions doing SELECT then UPDATE to a computed value against the same account under READ COMMITTED and observe one update lost.
- Replace it with the single atomic conditional UPDATE and re-run; then run the original read-modify-write under REPEATABLE READ with a bounded retry on 40001.
- Measure committed transactions per second against the single pooled row at 1, 8, 32 and 128 concurrent writers, recording mean lock hold time.
- Shard into 16 sub-rows hashed on transaction_id, re-measure, and write the query that answers the true available balance.
- Run concurrent opposing two-account transfers with and without ascending account_id lock ordering.
Follow-up
- While the balance is sharded, what query answers the account's true available balance, and what does it cost?
- A transaction has legs in two currencies. What does the balanced-to-zero check mean now, and where does the rounding residual go?
- What does the nightly reconciliation compare the sharded balance against?
Authorisation p99 tripled overnight with no deploy
Payment orchestration serves about 3,000 authorisations per second against a 150 ms p99 budget. Since 02:00, p99 is 460 ms and rising about 8 ms per hour, while p50 is unchanged at 11 ms. There was no deploy, no traffic change and no processor degradation. The hot path does one INSERT into idempotency_key, which has UNIQUE (scope, key), then two UPDATEs on that row: locked_at, then status and response_body. A nightly reconciliation job started at 01:50 and is still running. Give the ordered diagnostic checklist and the cause.
Approach
- Read the shape first. p50 flat with p99 rising and no deploy is a resource or data-volume effect, not a code path, because a code change moves the median too. A tail that climbs monotonically at fixed workload means something monotonically grows.
- Ask what started at 01:50. In PostgreSQL an open transaction holds back the xmin horizon cluster-wide, so autovacuum can reclaim no dead tuple newer than that snapshot. Confirm with pg_stat_activity (state, now() - xact_start, backend_xmin) and with pg_stat_all_tables (n_dead_tup, last_autovacuum) for idempotency_key.
- Connect it to the write pattern. Three writes per key produce up to two dead tuples each, so at 3,000 rps the table sheds roughly 6,000 dead tuples per second. Heap-only tuple updates would keep those out of the index, but only when no indexed column changes and the page has room, and appending response_body grows the tuple enough to force a new page. So the unique index on (scope, key) grows too.
- Explain why only the tail suffers. A larger index means more pages per lookup and a rising fraction of them missing shared_buffers; the median request still hits cache while the tail pays physical I/O. This is exactly the p50-flat, p99-rising signature, and it is worth stating before acting.
- Verify before fixing rather than after. n_dead_tup in the millions and rising, last_autovacuum stale since about 01:50, and pg_relation_size on the unique index measured twice fifteen minutes apart showing growth at constant workload. Index bloat cannot be inferred from row count alone; use the size series or pgstattuple.
- Fix in two moves and prevent separately. End or chunk the long transaction so the reconciliation job commits per batch instead of holding one snapshot over 50M lines, then let autovacuum catch up or run REINDEX CONCURRENTLY. Add a transaction-age alert and a statement timeout on the reporting role. Collapsing the two UPDATEs into one and lowering fillfactor halves dead-tuple production, but that is an optimisation, not the cause.
Follow-up
- The reconciliation job legitimately needs a consistent view of 50M lines. How do you give it one without pinning the xmin horizon?
- Why did p50 not move at all?
- You also run an expires_at cleanup job that DELETEs old idempotency keys. During this incident, does running it help or hurt, and what design avoids the question entirely?
For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Set up the IDE and build the banking service test-first
- Configure your IDE with a test runner and a template project that compiles and runs one passing test, so live-coding setup costs nothing
- Build an in-memory banking service with create, deposit, withdraw and transfer, writing a failing test for each rejection rule before the code that enforces it
- Decide between BigDecimal and integer minor units for balances, and write two sentences defending the choice for multi-currency accounts
Deliverable: A tested banking service where every invalid transaction is rejected atomically and leaves balances untouched.
Practice prompt ↗Practice prompt ↗Practice prompt ↗Worked solution ↗02Coding fundamentals: random output, custom structures, load balancing
- Generate a mine grid with an injected random source, placing k mines by selection sampling, and test invariants (exactly k mines, correct neighbour counts) rather than layouts
- Implement a Set of integers without Java collections, hash tables or trees, for example a sorted array with binary search, and state the cost of add, contains and remove
- Implement a load balancer with Random and Round Robin selection behind one interface, with a seeded random source for tests
Deliverable: Three solutions with tests, each with its time and space complexity written above the main method.
Practice prompt ↗Practice prompt ↗Practice prompt ↗03Concurrency: transfers, shared state and lock choice
- Make the Day 1 transfer thread-safe with per-account locks acquired in ascending id order, then run a stress test of opposing transfers and assert total money is conserved
- Make the Round Robin selector thread-safe with an AtomicInteger and Math.floorMod, and explain why a synchronized method would also be correct but slower under contention
- For a shared cache written by several worker threads, compare a synchronized map, ConcurrentHashMap.compute and a ReadWriteLock, and say when each is the cheapest correct choice
Deliverable: A stress test that fails with naive locking and passes with ordered locking, plus a one-page note on JVM concurrency primitives.
Practice prompt ↗Practice prompt ↗Practice prompt ↗04Database theory: isolation, locking and indexes
- Fill in a PostgreSQL grid of isolation levels against anomalies and explain each cell aloud
- Reproduce a lost update in two psql sessions, then fix it with an atomic conditional UPDATE, SELECT ... FOR UPDATE and a version column
- Work the drill 'Make a charge endpoint safe under concurrent duplicate retries' and explain B-Tree versus BRIN for a ledger table in three sentences
Deliverable: Session transcripts showing the lost update and each fix, and short spoken answers on isolation, locking and index choice.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Databases under load: contention, scaling and incidents
- Work the drill 'Raise the write ceiling on one hot clearing account', stating the throughput ceiling as a number before any design
- Compare sharding, partitioning and replication for a write-heavy banking database: what each scales and what each costs in consistency
- Work the debugging drill on authorisation p99, writing the diagnostic checklist in order before reading the cause
Deliverable: A written ceiling calculation, a scaling comparison table, and an ordered diagnostic checklist.
Practice prompt ↗Practice prompt ↗06System design: external APIs, distributed transactions and events
- Design the apartment booking system with unreliable third-party APIs end to end, marking where double booking is prevented
- Explain the Transactional Outbox with Kafka from local write to deduplicating consumer
- Compare an orchestrated saga with 2PC on one transfer, and sketch a ledger using CQRS and Event Sourcing
Deliverable: One full booking design and two short written explanations you can deliver aloud in five minutes each.
Practice prompt ↗Practice prompt ↗07Timed rehearsal of all four stages
- Solve one coding question from Day 2 in 45 minutes with tests, working in your own IDE and narrating as you go
- Answer ten database and concurrency theory questions aloud, one or two minutes each, naming the anomaly or failure each mechanism prevents
- Run one 45-minute banking design from Day 6 on a new prompt, such as a card ordering feature, and rehearse your screening summary and constraints
Deliverable: A timed coding solution with tests, a list of theory answers that needed a second attempt, and one recorded design walkthrough.
Practice prompt ↗Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
The sources do not describe a separate behavioural stage, but the reported question bank includes ownership and planning questions, such as preventing double-withdrawal incidents and planning an observability overhaul. Prepare stories where you owned a correctness problem end to end, with the specific change that followed.
Reverse a sharding decision after production contradicted it
To raise throughput past a single row's lock ceiling, you shard a hot settlement account balance into 16 sub-rows. Two weeks later the floor check has to sum all 16 under a stronger isolation level, contention has moved rather than gone, and operations cannot explain the balance to an auditor. Describe a decision you reversed. State what you believed when you made it, the measurement that changed your mind, how you unwound it without causing a second incident, and how long you waited before concluding the data was real rather than noise.
Approach
- The probe is whether you can hold a belief loosely and unwind your own work without ego. Begin with the reasoning that was correct at the time: a single balance row commits at roughly one write per lock hold, so at a 4 ms hold you get about 250 writes per second regardless of core count, and sharding is the standard answer to that ceiling.
- Name what the original reasoning missed rather than calling it a mistake in general. The floor predicate was a single-row CHECK before sharding and became a cross-row predicate after it, so every write now either sums the shards under SERIALIZABLE with a bounded retry on 40001 or locks them in a fixed order to avoid 40P01. The throughput gain is real but smaller than 16x, and the auditability cost was never priced.
- Give the measurement that decided it, with a before and an after: committed writes per second, p99 write latency, retry rate on 40001, and the time an analyst needs to reconstruct one balance. A reversal justified by feel is the generic answer.
- Describe the unwind as a migration, not a revert: shadow the consolidated balance, reconcile it against the sum of shards over a full business day including the cutoff, cut reads over first, then writes, keeping the shards readable until one full reconciliation cycle has passed clean.
- State the waiting rule you used before acting. Two weeks of a moving p99 can be a deploy or a traffic shift; a strong answer names the signal that separated a trend from noise, such as the retry rate persisting across a low-traffic weekend.
- Close with what you would keep. Some of the work is usually salvageable (the instrumentation, the lock ordering, the measured ceiling), and saying which parts survived shows the reversal was analysed rather than abandoned.
Follow-up
- You still need the throughput. What is the next thing you try, and what does it cost the floor check?
- How do you reconcile the sharded balance against the consolidated one during the migration without double counting entries posted mid-cut?
- What would you have measured before the original change that would have made the answer obvious?
Force an implicit timeout behaviour into an explicit decision
The risk decision service has an 80 ms p99 budget inside a roughly 2 s caller timeout. Today, when its feature store is unavailable, the timeout handler returns approve. Nobody chose that; it is what the code does. You need a real decision: fail open, fail closed, or refer, potentially differing by amount band. Describe a time you turned an accidental behaviour into an owned decision. State who had to be in the room, the data you brought, what you did when nobody wanted to own it, and where the decision was recorded so it outlived you.
Approach
- The probe is whether you can drive a cross-functional decision rather than escalating and waiting. Lead with the framing that makes it undeniable: this is already a product decision, it is currently being made by an exception handler, and the only question is whether anyone reviews it.
- Bring the two losses side by side instead of arguing a principle. Fail open costs expected fraud loss on approved-but-should-have-declined volume during the outage; fail closed costs declined good payments, which is lost revenue plus customer harm and a support queue; refer costs manual review capacity, which is a headcount number and saturates within minutes at 3,000 decisions per second. Give each as a rate per minute of outage using real volume.
- Propose the banded answer as the default, because the two losses cross over at an amount: below some threshold the expected fraud loss is smaller than the expected decline loss, above it the reverse, and the crossover is computable from observed fraud rate by band. That converts a values argument into an arithmetic one.
- Name the attendees by the decision they own, not by title: whoever carries fraud loss, whoever carries approval rate, and whoever staffs manual review. Three people who can each say yes is a decision; eight people who can each say no is a meeting.
- Say what you did when ownership was contested. A strong answer has a forcing function: propose a default in writing with a review date and state that it ships unless someone objects, which converts inaction into consent rather than into another meeting.
- Record it where the code can find it: the decision, its date, its owner, the amount thresholds, and a test asserting the fallback behaviour, so the next engineer reading the timeout handler learns it was chosen. A wiki page nobody links from the code is the generic answer.
Follow-up
- The feature store is degraded rather than down and the model is scoring on stale features. Is that the same decision?
- How do you stop the banded thresholds from silently rotting as fraud patterns shift?
- Nobody objects to your written default, and six months later there is an outage and a loss. Who owns it?
Unblock an engineer on double-posted interest accrual
An engineer two years into their career has a nightly accrual job that double-posts interest for some accounts whenever the batch is partially re-run after a failure. They have spent two days on it and are now rewriting the batch runner. You have 30 minutes. Describe how you have unblocked someone without taking the keyboard: the question you asked first, how you chose between handing over the answer and handing over the method, what you left behind so the next person does not get stuck here, and how you knew they were unblocked rather than deferring to you.
Approach
- The probe is whether you grow people or absorb their work. Open with the diagnostic question rather than the solution: ask what identifies one unit of work, because the answer reveals immediately that accrual is keyed by (account_id, accrual_date) and that the job has no uniqueness on it.
- Redirect from the runner to the write. The rewrite is aimed at never re-running, which is unachievable; the property needed is that re-running posts nothing new, enforced by a unique index on (account_id, accrual_date) or by passing the same idempotency key to the ledger posting operation so the second attempt is a no-op rather than a second transaction.
- Choose deliberately between answer and method and say why. Two days in and blocked on the wrong layer is usually the moment to hand over the framing (restartable at account granularity, idempotent per unit) and let them write the code, because the lesson is the framing and the code is the easy part.
- Leave an artefact, not a conversation: a test that re-runs one account twice and asserts one posting, plus two lines in the runbook stating that per-account work must be idempotent because the batch is always partially re-run.
- Check that they are unblocked by asking them to predict the failure that the fix does not cover, such as a mid-run rate change producing two different correct amounts for the same key. If they can find the next edge themselves, they own it; if they ask you to confirm each step, they are deferring and you have hidden the block rather than removed it.
- Say what you deliberately did not do. Not fixing it yourself before the standup is the whole exercise, and a strong answer names the pressure it resisted.
Follow-up
- The unique index rejects the re-run, but the first run posted the wrong amount. How should the job behave now?
- How do you tell whether you taught them or just unblocked them, a month later?
- The same engineer is blocked again next week on a similar problem. What does that tell you about your first intervention?
- 01
Tell me about a payment-critical or data-integrity incident you owned. What broke, how did you find the cause, who did you bring in, and what did you change so it could not happen again?
- 02
You have nine months to overhaul observability for a set of backend services. What do you deliver first, what do you defer, and how do you show progress along the way?
- 03
Describe a design trade-off you made under time pressure. Which alternative did you reject, and what would have made you choose it instead?
- 04
Tell me about a test you wrote that caught a bug before production. What made you write that test?
- 05
Describe a technical decision you later reversed. What did you believe at the time, and what measurement changed your mind?
Is this an official Revolut interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Revolut. Rounds and questions reflect what candidates have reported, not a process Revolut has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How difficult are the technical interviews at Revolut?
The coding problems are reported as generally medium in algorithmic difficulty. The bar comes from tight time limits and strict criteria on code quality, TDD, concurrency and production readiness.
PracHub interview research ↗Which programming language should I use during live coding?
Java and Kotlin are reported as the main backend languages and are recommended for live coding. You can generally use another language you are comfortable with, provided you can show solid concurrency handling and TDD. Confirm with your recruiter before the hands-on stage.
PracHub interview research ↗What is the main point of failure for candidates in technical rounds?
Reported failure points are rushing to write code without setting up tests, ignoring edge cases, failing to explain concurrency trade-offs, and lacking depth on database isolation levels and locking.
PracHub interview research ↗How long is the live coding exercise?
Reports say you will often have 30 to 45 minutes to complete a live coding exercise, write unit tests and answer quick theory questions. Practise finishing a tested solution inside 45 minutes.
PracHub Software Engineer practice ↗What is the typical timeline from the initial screen to an offer?
Reports put it at roughly three to five weeks from first contact to offer. Ask your recruiter for the current schedule.
PracHub interview research ↗Sources & methodology 3 sources ↗
Official role evidence, timestamped platform data and clearly labeled preparation advice.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
platform · Accessed 2026-09-24 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
platform · Accessed 2026-09-24 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
platform · Accessed 2026-09-24