Revolut System Design Interview Prep

Web Engineer • Full prep guide with checklists, diagrams, and practice material

0 / 0 items checked

Interview Timeline

0-5 min
Introduction — Brief intro, interviewers set context
5-15 min
Requirements Gathering — Ask questions, define scope, functional + non-functional
15-25 min
High-Level Architecture — End-to-end data flow diagram
25-45 min
Web Architecture Detail — Frontend deep dive (biggest section!)
45-55 min
API Contracts — Endpoints, idempotency, error formats
55-60 min
Wrap-up — Failure scenarios, testing, questions

Phase 1: Requirements Gathering 10 min

This sets the tone for the whole interview. Be structured. Don't rush into drawing.

What are Functional Requirements?

What the system does. These describe the features and behaviors a user can see and interact with. Think of them as the "user story" answers.

Example: "A user can send money to another user" / "The system shows a transaction history" / "The system converts currencies in real-time."

If you removed a functional requirement, the user would notice something missing.

What are Non-Functional Requirements?

How the system behaves. These describe qualities and constraints that users feel but don't directly interact with. They're the "-ilities": scalability, reliability, security, performance.

Example: "The system handles 10,000 requests/second" / "Pages load in under 2 seconds" / "Data is encrypted at rest and in transit" / "99.9% uptime."

If you violated a non-functional requirement, the system would still work but would be slow, insecure, or crash under load.

Functional Requirements

What the system does

Non-Functional Requirements

How the system behaves

What does "Real-time vs Eventual Consistency" mean?

Real-time (strong) consistency: When you send money, the balance updates instantly everywhere. If you check on another device 1 millisecond later, you see the new balance. This is slower because all parts of the system must agree before responding.

Eventual consistency: After you send money, there's a brief delay (milliseconds to seconds) before all parts of the system reflect the new state. Faster and more scalable, but you might briefly see stale data.

In a payment system you typically want both: Strong consistency for the transaction itself (don't allow overdraft — the balance must be accurate at the moment of transfer). Eventual consistency is fine for displaying transaction history (ok if it appears 1-2 seconds later on another screen).

This is a great question to ask the interviewer: "For the core transfer operation, do we need strong consistency, or is eventual consistency acceptable?" It shows you understand the trade-off between speed/scalability and data accuracy.

Questions to Always Ask

Sample Requirements Conversation

This is how the first 10 minutes should feel. Read it out loud to practice the rhythm.

Money Transfer
Flight Booking
Streaming Dashboard

Interviewer: "Design a money transfer system for Revolut Business."

You: "Great. Before I start drawing, I'd like to understand the scope. Who's the primary user? A business admin sending bulk payments, or individual employees making one-off transfers?"

Interviewer: "Individual employees sending money to suppliers or other businesses."

You: "Got it. Is this international? Should we handle multi-currency with exchange rates?"

Interviewer: "Yes, international transfers with currency conversion."

You: "Understood. For scale — are we talking thousands of transfers per day, or millions? This affects whether we need message queues and async processing."

Interviewer: "Let's say tens of thousands per day."

You: "OK. Real-time question: after a user submits a transfer, should they see the status update live (pending → completed), or is it fine to poll?"

Interviewer: "Real-time would be ideal."

You: "Perfect. Let me summarize the requirements before we move on:"

Functional: User can create international transfers with currency conversion, view transfer status in real-time, see transaction history with filters, handle notifications on completion/failure.

Non-functional: Tens of thousands of daily transfers, real-time status via WebSocket, strong consistency for the transaction itself, ACID guarantees (no double-charging), PCI compliance, multi-region deployment for global users, sub-second response for API calls.

You: "Does this capture it, or should I adjust the scope?"

Interviewer: "Design a flight and hotel booking system."

You: "Interesting. A few questions to scope this right. Is this within the Revolut app — so users pay with their Revolut balance? Or standalone?"

Interviewer: "Within Revolut. Users pay with their balance."

You: "Search: Do we aggregate from multiple providers (like Skyscanner), or a single provider API?"

Interviewer: "Multiple providers, we aggregate results."

You: "Booking hold: When a user selects a flight, do we need to hold/reserve it for X minutes while they complete payment? Prices and availability change fast."

Interviewer: "Yes, a temporary hold makes sense."

You: "Offline / persistence: Should search results be cached? If the user navigates away and comes back, do we keep their search?"

Interviewer: "Yes, cache recent searches."

You: "OK, let me summarize:"

Functional: Search flights/hotels from multiple providers, filter/sort results, select and hold a booking temporarily, multi-step checkout (passengers → payment → confirm), booking confirmation + e-ticket, booking history.

Non-functional: Aggregated search across providers (latency-sensitive — need parallel requests + timeouts), cached search results (short TTL, prices change), reservation expiry with countdown timer, payment via Revolut balance (idempotency!), high availability (users expect booking to work 24/7).

Interviewer: "Design a real-time analytics dashboard for business transactions."

You: "Got it. Who's the audience? A business owner looking at their own transactions, or an internal Revolut operations team monitoring all transactions?"

Interviewer: "Business owner — their own company's transaction data."

You: "What kind of visualizations? Time series charts (spend over time), aggregate cards (total spent this month), breakdowns (by category/currency)?"

Interviewer: "All of those. Time series is the most important."

You: "How real-time? Should a new transaction appear on the chart within seconds, or is a 5-minute refresh acceptable?"

Interviewer: "Within seconds for the latest data."

You: "Data volume: How far back? Live data + last 30 days? Or years of history?"

Interviewer: "Live data plus last 12 months. Older data can be slower to load."

Functional: Real-time time series chart (spend over time), aggregate summary cards, breakdown by category/currency/recipient, date range picker, export to CSV.

Non-functional: Sub-second updates for live data (WebSocket/SSE), historical data loads in < 2s (pre-aggregated), handle large datasets efficiently (data windowing, virtualization), responsive for different screen sizes, graceful degradation when WebSocket disconnects (fall back to polling).


Phase 2: High-Level Architecture 10 min

Draw the full system. Show how data flows from database through backend to the client. The browser has two separate paths: one to the CDN for static assets, one to the API Gateway for data.

Money Transfer
Flight/Hotel Booking
Streaming Dashboard
Drawing Tips

High-Level Architecture — Revolut Business International Money Transfer

Note on Load Balancer vs API Gateway: In this diagram they are merged into one box. Modern API Gateways (Kong, AWS API Gateway, Nginx) include load balancing built in. For simplicity in the interview, you can keep them as one. If asked, mention: "In production at scale, the load balancer and API gateway might be separate layers — the LB distributes traffic across multiple API gateway instances."

Know Why Each Component Exists

Payment-specific additions: Idempotency layer at the API Gateway or service level. Transaction ledger with ACID guarantees. Payment processor integration (internal or Stripe-like).

High-Level Architecture — Flight/Hotel Booking System

Key differences from Money Transfer:

High-Level Architecture — Real-Time Analytics Dashboard

Key differences from Money Transfer:

How to Draw This in 10 Minutes on Excalidraw


Phase 3: Frontend Architecture Deep Dive 20 min — BIGGEST SECTION

Zoom into the client box from the high-level diagram. This is where you spend the most time.

Money Transfer
Flight Booking
Dashboard

Frontend Architecture — Revolut Business Web App (Money Transfer)

Frontend Architecture — Flight/Hotel Booking

Key frontend differences for booking:

Frontend Architecture — Real-Time Analytics Dashboard

Key frontend differences for dashboard:

State Management
Loading States
Error Handling
Auth Flow
Performance
Dev Experience

Why Separate API Data from UI State?

In a client-side SPA, all state lives on the client. But it's useful to separate it by where it originates:

// Remote data with a caching layer (e.g. React Query, SWR, or custom)
const { data: balance, isLoading } = useQuery({
  queryKey: ['balance', accountId],
  queryFn: () => api.getBalance(accountId),
  staleTime: 30_000, // consider fresh for 30s
  refetchOnWindowFocus: true,
});

// After a successful transfer, invalidate cached data so it refetches
const mutation = useMutation({
  mutationFn: api.createTransfer,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['balance'] });
    queryClient.invalidateQueries({ queryKey: ['transactions'] });
  },
});

Loading States — They Specifically Asked About This!

Key insight: Every user interaction should have immediate visual feedback. The user should never wonder "did my click work?"

Error Handling Strategy

// Error boundary per module
<ErrorBoundary fallback={<TransferErrorFallback />}>
  <TransferModule />
</ErrorBoundary>

// API interceptor
api.interceptors.response.use(null, (error) => {
  if (error.response?.status === 401) {
    return refreshTokenAndRetry(error.config);
  }
  return Promise.reject(normalizeError(error));
});

Authentication & Authorization

Performance

Developer Experience


Phase 4: API Contracts Design 10 min

Design endpoints for the money transfer system. Focus on clarity, reliability, and maintainability.

POST /api/v1/transfers — Create a new money transfer

Request Headers

Authorization: Bearer <token>
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

Request Body

{
  "from_account_id": "acc_123",
  "to_account_id": "acc_456",
  "amount": 10000,
  "currency": "EUR",
  "description": "Rent payment"
}

Response 201 Created

{
  "data": {
    "id": "txn_789",
    "status": "pending",
    "amount": 10000,
    "currency": "EUR",
    "from_account_id": "acc_123",
    "to_account_id": "acc_456",
    "created_at": "2026-04-08T10:00:00Z"
  }
}

Response Headers (from server)

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1712570400
Idempotency-Key: The client generates a UUID before sending the request. If the network fails and the client retries with the same key, the server recognizes it and returns the cached response instead of processing a second payment. New transfer intent = new key. Retry of same transfer = same key.
GET /api/v1/transfers/:id — Get transfer details & status

No request body. Used to poll transfer status after creation.

No Idempotency-Key needed — GET is naturally idempotent (reading data never changes anything).

Response 200

{
  "data": {
    "id": "txn_789",
    "status": "completed",
    "amount": 10000,
    "currency": "EUR",
    ...
  }
}
GET /api/v1/transfers?page=1&limit=20&status=completed — List transfers (paginated)

Query Parameters

page     = 1          // page number
limit    = 20         // items per page
status   = completed  // filter (optional)
currency = EUR        // filter (optional)
from     = 2026-01-01 // date range (optional)

Response 200

{
  "data": [ ...transfers ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 142,
    "has_next": true
  }
}
GET /api/v1/accounts/:id/balance — Check account balance
Response 200:
{
  "data": {
    "account_id": "acc_123",
    "balance": 250000,     // minor units: 2500.00 EUR
    "currency": "EUR",
    "updated_at": "2026-04-08T10:00:00Z"
  }
}
ERROR Consistent error format for all endpoints
// HTTP 400 / 401 / 403 / 409 / 429 / 5xx
{
  "error": {
    "code": "INSUFFICIENT_FUNDS",        // machine-readable, for frontend logic
    "message": "Account balance is too low", // human-readable, for display
    "details": {                          // optional context
      "available": 5000,
      "required": 10000
    }
  }
}

API Design Checklist

Rate Limiting Headers — What Are They?

These are RESPONSE headers — the server sends them back to you

Rate limiting protects the server from being overwhelmed. The server tracks how many requests each client makes and includes these headers in every response so the client knows where it stands:

X-RateLimit-Limit: 100 — "You're allowed 100 requests per time window"
X-RateLimit-Remaining: 97 — "You have 97 requests left in this window"
X-RateLimit-Reset: 1712570400 — "Your limit resets at this Unix timestamp"

When remaining hits 0: The server responds with 429 Too Many Requests and a Retry-After: 30 header telling the client to wait 30 seconds.

Frontend should: Read these headers, show a user-friendly message if rate limited ("Please wait a moment before trying again"), and implement exponential backoff with jitter for automatic retries.

Exponential Backoff + Jitter

How to retry failed/rate-limited requests without making things worse

The problem: If 1000 clients all get rate-limited at the same time and all retry after exactly 2 seconds, they'll all hit the server again simultaneously — creating a "thundering herd" that crashes it again.

Exponential backoff: Each retry waits longer than the last: 1s → 2s → 4s → 8s → 16s (doubling each time, with a max cap).

Jitter: Add a random delay on top so not everyone retries at the exact same moment. Instead of all clients retrying at 4s, one retries at 3.2s, another at 4.7s, another at 5.1s — spreading the load.

// Exponential backoff with jitter
function getRetryDelay(attempt, baseMs = 1000, maxMs = 30000) {
  const exponential = baseMs * Math.pow(2, attempt);  // 1s, 2s, 4s, 8s...
  const capped = Math.min(exponential, maxMs);         // never more than 30s
  const jitter = capped * (0.5 + Math.random() * 0.5); // add randomness
  return jitter;
}

// attempt 0 → ~1.0-1.5s
// attempt 1 → ~2.0-3.0s
// attempt 2 → ~4.0-6.0s
// attempt 3 → ~8.0-12.0s (spreading the retries)

When to give up: Set a max number of retries (e.g. 3-5). After that, show the user an error: "Transfer couldn't be completed. Please try again later." Don't retry forever.

Circuit Breaker Pattern

Stop calling a service that's clearly down — give it time to recover

Think of it like an electrical circuit breaker in your house: when there's too much load, it trips and cuts the connection to prevent damage.

Three states:

CLOSED (normal) — Requests flow through normally. The circuit tracks failure count.
OPEN (tripped) — Too many failures detected (e.g. 5 failures in 10 seconds). Requests are immediately rejected without calling the server. Show cached data or a fallback UI. This gives the server time to recover instead of hammering it with more requests.
HALF-OPEN (testing) — After a timeout (e.g. 30 seconds), let ONE request through to test if the service recovered. If it succeeds → back to CLOSED. If it fails → back to OPEN.

On the frontend this means: If the balance API fails 3 times in a row, stop calling it. Show the last cached balance with a note "Balance may be outdated" and a manual "Retry" button. After 30 seconds, try again automatically.

In the interview, mention it when discussing: failure scenarios, graceful degradation, or how the frontend handles a backend outage.

Idempotency — Why POST Needs Special Handling

The problem: POST is NOT idempotent by nature

Idempotent means "doing it twice has the same effect as doing it once."

GET /transfers/123 — Naturally idempotent. Reading data 10 times changes nothing.
PUT /transfers/123 {amount: 100} — Naturally idempotent. Setting a value to 100 twice still gives 100.
DELETE /transfers/123 — Naturally idempotent. Deleting twice? Already gone after the first.
POST /transfers {amount: 100}NOT idempotent! Sending twice = two transfers = user charged twice!

The fix: The client attaches an Idempotency-Key header (a UUID it generates). The server uses this key to detect retries and return the original response without processing again.

Idempotency Flow — Preventing Double Charges on Retry


Phase 5: Failure Scenarios & Testing 5 min

Failure Scenarios

Testing Strategy

Metrics to Measure

Core Web Vitals — Google's 3 metrics for user experience

These are measured in the user's real browser and reported back. Google uses them for search ranking too.

LCP = Largest Contentful Paint — How long until the biggest visible element (hero image, main text block) appears on screen. Measures "when does the page look loaded?"
Good: < 2.5s   Needs work: 2.5-4s   Poor: > 4s

INP = Interaction to Next Paint — How long between the user clicking/tapping something and the screen visually responding. Measures "does the page feel responsive?"
Good: < 200ms   Needs work: 200-500ms   Poor: > 500ms

CLS = Cumulative Layout Shift — How much the page content jumps around while loading (e.g. an image loads late and pushes text down, or an ad inserts above the button you were about to click). Measures "does the layout stay stable?"
Good: < 0.1   Needs work: 0.1-0.25   Poor: > 0.25

API Latency Percentiles — p50, p95, p99

Instead of "average response time" (which hides outliers), we use percentiles:

p50 (median) — 50% of requests are faster than this. This is your typical experience.
p95 — 95% of requests are faster than this. Only 1 in 20 is slower. This is what most users experience at worst.
p99 — 99% of requests are faster than this. Only 1 in 100 is slower. This catches the worst-case outliers.

Example: "Our transfer API has p50 = 120ms, p95 = 450ms, p99 = 1200ms" means: most requests complete in 120ms, but 1 in 100 takes over a second — probably worth investigating why.

Why not average? If 99 requests take 100ms and 1 takes 10,000ms, the average is 199ms — looks fine. But p99 = 10,000ms tells you something is badly wrong for some users.

In the interview, say: "We'd monitor p50, p95, and p99 latency — averages hide problems."

Apdex — Application Performance Index

A single number from 0 to 1 that summarizes user satisfaction based on response time.

You define a threshold T (e.g. T = 500ms). Then:

Satisfied — response < T (under 500ms)
Tolerating — response between T and 4T (500ms - 2s)
Frustrated — response > 4T (over 2s) or errors

Apdex = (Satisfied + Tolerating/2) / Total. Score of 1.0 = everyone happy. Score of 0.5 = half your users are frustrated. Simple way to answer "are users happy with performance?"

Where and How to Collect Metrics


Rules & Gotchas

DO NOT say: "We already did something similar at my previous company" — they explicitly said to avoid this. Focus on engineering the solution from scratch.
DO NOT rely on specific technologies. Say "a component library" not just "Material UI". Say "data-fetching with caching" not just "React Query". You can mention specific tools as examples, but don't anchor your design on them.
DO NOT jump between topics. Follow the interview phases in order. Finish requirements before drawing. Finish high-level before zooming in.
DO ask questions whenever in doubt. "Is it okay if I focus on the transfer flow first?" "Should I also consider mobile?" There is no limit on questions.
DO think global. Revolut is in 35+ countries. Mention: i18n, multi-currency, time zones, RTL languages, regional compliance.
RTL Languages (Right-to-Left)

Languages like Arabic, Hebrew, and Farsi are read and written from right to left. This flips the entire UI layout:

What changes in RTL mode: How to mention in interview: "We set dir="rtl" on the HTML element and use CSS logical properties so the entire layout mirrors automatically. We avoid hardcoded left/right in CSS."
DO keep it simple. A working solution beats an overcomplicated one. They said this explicitly.
DO show customer focus. Frame every decision as user benefit: "This gives faster feedback to the user" / "This prevents double-charging the customer."
DO handle loading states everywhere. They specifically flagged this as important in feedback.
Loading States — Beyond Spinners

Showing a spinner for every loading state is a bad UX pattern. It makes the app feel slow and causes layout shift (bad for CLS). Here are better approaches:

1. Skeleton screens (best for initial loads)
Show grey placeholder shapes that match the layout of the content about to appear. The user sees the page "structure" instantly, then content fills in. No layout jump. 2. Optimistic UI (best for user actions)
Immediately show the expected result before the server confirms. If the server fails, roll back and show an error. 3. Stale-while-revalidate (best for cached data)
Show the last known data immediately, then fetch fresh data in the background. When the new data arrives, update silently. 4. Progressive / incremental loading
Load and render critical content first, then load secondary content after. 5. Inline loading indicators (when you must show "loading")
Small, contextual indicators right where the action happened — not a full-page overlay. How to mention in interview: "For loading states, I'd use skeleton screens for initial page loads to avoid layout shift, optimistic updates for user actions like transfers, and stale-while-revalidate for cached API data so the screen is never blank. Full-page spinners are a last resort."

Communication Checklist


Resources

Required Reading/Watching (from Revolut)

Highly Recommended

Bonus (if time allows)


Practice Problems

These are the most likely problem types for Revolut. Practice at least one end-to-end on Excalidraw.

Most Likely Design a Money Transfer System (Revolut Business)

Scenario: A business user wants to send money to a supplier in another country.

Key Considerations

Likely Design a Flight/Hotel Booking System

Scenario: User searches for flights, selects one, books and pays.

Key Considerations

Possible Design a Data Streaming Dashboard

Scenario: Real-time dashboard showing business transaction analytics.

Key Considerations


Check items as you study them. Progress is saved in your browser.

Good luck! You've already passed the earlier stages — they like you.