Skip to content
RED SAGSkills

Backend Developer interview questions and answers

30 questions interviewers really ask, with what a strong answer covers. Read them, then test yourself in a timed mock interview.

Start a Backend Developer mock interview

Easy questions easy

1. How would you design a REST API for a simple blog with posts and comments? Mention URLs, methods and status codes.

A strong answer covers:

  • Resource-based URLs using nouns, such as /posts and /posts/{id}/comments
  • Correct HTTP methods: GET, POST, PUT/PATCH, DELETE
  • Meaningful status codes such as 200, 201, 400, 404
  • Pagination and filtering for list endpoints
  • Validation and consistent JSON error format
Show model answer

I would model posts and comments as resources with noun URLs. GET /posts lists posts with pagination, for example ?limit=20&cursor=abc, and supports filters like ?author=5. POST /posts creates a post and returns 201 Created with the new post. GET /posts/{id} returns one post or 404 if it does not exist. PATCH /posts/{id} updates some fields and DELETE /posts/{id} removes it, returning 204. Comments are nested: GET /posts/{id}/comments and POST /posts/{id}/comments. I would validate input and return 400 with a consistent JSON error body such as { error: { code, message, fields } }. Writes need authentication, so 401 for no token and 403 when the user is not the author. I would also version the API, for example /v1, and document it with OpenAPI.

2. When would you choose a SQL database and when a NoSQL database?

A strong answer covers:

  • SQL suits structured, related data and needs strong consistency and transactions
  • NoSQL suits flexible schemas, very high write scale or specific access patterns
  • Different NoSQL types: document, key-value, wide-column, graph
  • Choice depends on query patterns and consistency needs, not hype
Show model answer

I start from the data and the queries. If the data is structured and related, like users, orders and payments, and I need joins and ACID transactions, a relational database like PostgreSQL is usually the best default. It also handles JSON columns well, so it covers many semi-structured cases. I would pick NoSQL when the access pattern fits it better: a document store like MongoDB for flexible, nested records read as a whole; a key-value store like Redis for caching and sessions; a wide-column store like Cassandra or DynamoDB for huge write volumes with known key-based queries; or a graph database for relationship-heavy queries. NoSQL often trades joins and strict consistency for easier horizontal scaling. Many real systems use both, for example PostgreSQL as the source of truth and Redis as a cache.

3. What is the difference between authentication and authorization? How would you implement both in an API?

A strong answer covers:

  • Authentication verifies who the user is
  • Authorization decides what the user is allowed to do
  • Sessions or tokens (JWT) for authentication; hashed passwords
  • Role-based or permission checks on every protected endpoint, done on the server
  • Correct 401 vs 403 responses
Show model answer

Authentication answers who you are; authorization answers what you can do. For authentication, the user logs in with email and password, I check the password against a bcrypt or Argon2 hash, and then issue either a session cookie (HttpOnly, Secure) or a short-lived JWT with a refresh token. Middleware validates this on each request and attaches the user to the request. If it is missing or invalid, I return 401. For authorization, I check permissions on the server for every protected action, for example role-based access where only admins can delete users, plus ownership checks so a user can edit only their own posts. If the user is known but not allowed, I return 403. I never trust the frontend to hide buttons as the only protection.

4. Explain what happens on the backend from the moment a request hits your server until a response is sent.

A strong answer covers:

  • DNS, TCP/TLS connection and load balancer route the request
  • Web server or framework parses the request and runs middleware
  • Routing to a handler, validation, business logic
  • Database or cache calls, then building the response
  • Status code, headers, body; logging and metrics
Show model answer

The client first resolves the domain through DNS and opens a TCP connection with TLS. The request usually reaches a load balancer or reverse proxy like Nginx, which forwards it to one of the app instances. The framework parses the method, URL, headers and body, then runs middleware such as logging, CORS, rate limiting and authentication. The router matches the URL to a handler. The handler validates input, runs business logic, and reads or writes data in the database or cache, possibly calling other services. It then builds the response with a status code, headers like Content-Type and caching headers, and a JSON body. Error-handling middleware turns exceptions into proper error responses. Finally, the request is logged with its duration and status, and metrics and traces are recorded for monitoring.

5. What is a database index, and what are its costs? How do you decide which columns to index?

A strong answer covers:

  • Index is a data structure (usually B-tree) for fast lookups
  • Speeds up WHERE, JOIN and ORDER BY on indexed columns
  • Costs: extra storage and slower inserts and updates
  • Index based on real query patterns and selectivity
  • Use EXPLAIN to verify the index is used
Show model answer

An index is a separate data structure, usually a B-tree, that keeps column values sorted with pointers to rows. Instead of a full table scan, the database can seek directly to matching rows, which makes WHERE filters, joins and ORDER BY much faster. The costs are extra disk and memory, and slower writes, because every INSERT, UPDATE and DELETE must also update each index. So I do not index every column. I look at the slow and frequent queries, check them with EXPLAIN or EXPLAIN ANALYZE, and index columns used in filters and joins, especially foreign keys. Columns with high selectivity, like email, benefit most. For queries that filter on several columns I use a composite index with the most commonly filtered column first. Then I confirm with EXPLAIN that the plan really uses the index.

6. What is a database transaction, and what do the letters in ACID mean? Give an example where you need one.

A strong answer covers:

  • A transaction groups several operations into one unit of work
  • Atomicity: all or nothing; Consistency: constraints hold
  • Isolation: concurrent transactions do not see each other's partial work
  • Durability: committed data survives crashes
  • Concrete example such as a money transfer or order plus stock update
Show model answer

A transaction is a group of database operations that succeed or fail together. ACID describes its guarantees. Atomicity means all or nothing: if anything fails, the whole transaction rolls back. Consistency means the database moves from one valid state to another, so constraints like foreign keys and unique keys still hold. Isolation means concurrent transactions do not see each other's half-finished work; the isolation level, such as read committed or serializable, controls how strict that is. Durability means that once COMMIT returns, the data survives a crash, usually thanks to a write-ahead log. A classic example is a bank transfer: debiting one account and crediting another must happen together, or money disappears. Another is placing an order, where I insert the order rows and decrease stock in one transaction so they never disagree.

7. What are logs, metrics and traces, and how would you use each one to run a backend service?

A strong answer covers:

  • Logs are detailed records of individual events, best when structured
  • Metrics are numeric time series used for dashboards and alerts
  • Traces follow one request across services with timed spans
  • Correlate them with a shared trace or request id
  • Alert on symptoms users feel, such as error rate and latency
Show model answer

They are the three main signals of observability. Logs are records of single events, like "payment failed for order 9". I write them as structured JSON with fields such as level, user id and request id, so I can search and filter them. Metrics are numbers over time, such as request rate, error rate, latency percentiles and queue depth. They are cheap to store and are what I build dashboards and alerts on, for example alerting when p99 latency or the 5xx rate goes above a threshold. Traces follow one request through every service, showing how long each step took, so I can see that a slow request spent most of its time in one database query. I use OpenTelemetry to produce traces and put the trace id in every log line. Then an alert leads me to a trace, and the trace leads me to the exact logs.

8. How would you test a new POST /orders endpoint? Which kinds of tests would you write?

A strong answer covers:

  • Unit tests for business logic such as price calculation and validation
  • Integration tests with a real database, for example in Docker
  • API tests that check status codes, response body and errors
  • Cover edge cases: invalid input, missing auth, duplicates, out of stock
  • Mock external services and run tests in CI
Show model answer

I would follow the testing pyramid. First, unit tests for the pure logic, like calculating totals, applying discounts and validating input, with dependencies replaced by fakes. Next, integration tests that run the repository code against a real PostgreSQL in a container, so I know the SQL and constraints work. Then a few API tests that send real HTTP requests to the app and check the full behaviour: 201 with the new order and a Location header for valid input, 400 with clear field errors for bad input, 401 without a token, 403 for another user's resources, and 409 when stock runs out. External services like payments would be mocked or pointed at a sandbox. I make tests independent, with fresh data for each, and run them all in CI on every pull request so bugs are caught before merging.

9. Why do backend teams package services with Docker, and what makes a good Dockerfile?

A strong answer covers:

  • Same environment in development, CI and production
  • Images bundle the app and its dependencies; containers are isolated processes
  • Small base images and multi-stage builds
  • Order layers for caching and use a .dockerignore
  • Run as a non-root user and keep secrets out of the image
Show model answer

Docker packages the app with its runtime and dependencies into an image, so the same artifact runs the same way on a laptop, in CI and in production. That removes "works on my machine" problems and makes deployments and rollbacks simple, because you just run a different image tag. It also lets developers start dependencies like PostgreSQL and Redis with one compose file. A good Dockerfile uses a small, pinned base image, and a multi-stage build so build tools do not end up in the final image. I copy the dependency manifests first and install them before copying the source, so that layer stays cached. I add a .dockerignore to skip node_modules and .git. Finally I run the process as a non-root user and pass secrets and configuration through environment variables or a secret manager at runtime, never baking them into the image.

10. Explain the HTTP status code classes and give examples of codes a REST API commonly returns.

A strong answer covers:

  • 2xx success: 200, 201, 204
  • 3xx redirection and caching: 301, 304
  • 4xx client errors: 400, 401, 403, 404, 409, 422, 429
  • 5xx server errors: 500, 502, 503, 504
  • Correct codes help clients decide whether to retry
Show model answer

Status codes are grouped by their first digit. 2xx means success: 200 OK for a normal response, 201 Created after creating a resource, and 204 No Content when there is nothing to return, such as after a delete. 3xx is about redirects and caching, like 301 for a permanent move and 304 Not Modified for a valid cached copy. 4xx means the client did something wrong: 400 for bad input, 401 when not authenticated, 403 when authenticated but not allowed, 404 when the resource does not exist, 409 for a conflict like a duplicate, 422 for valid JSON that fails business rules, and 429 for rate limiting. 5xx means the server failed: 500 for an unexpected error, 502 and 504 from gateways, and 503 when temporarily unavailable. Using them correctly matters because clients and proxies decide on retries from them: retrying a 503 makes sense, retrying a 400 does not.

Medium questions medium

1. An API endpoint is slow because it runs a heavy database query. How would you add caching, and how do you keep the cache correct?

A strong answer covers:

  • Measure first and choose what to cache
  • Cache-aside with Redis and a TTL
  • Invalidation on writes: delete or update keys
  • Cache key design including user or tenant where needed
  • Handle stampedes and stale data risks; HTTP caching where possible
Show model answer

First I would measure, using EXPLAIN and traces, to see if the query can simply be fixed with an index. If it is still expensive and read often, I would use cache-aside with Redis: on a read, check the key; on a miss, run the query, store the result with a TTL, and return it. The key must include everything that changes the result, such as user id, tenant id and query parameters, otherwise one user may see another user data. On writes, I update the database and then delete the related keys, so the next read rebuilds fresh data; the TTL is a safety net. For hot keys I protect against stampedes with a short lock or serving stale while refreshing. For public data I would also use HTTP caching headers or a CDN. I would monitor hit rate and latency.

2. You need to rename a column in a large production table that the live app uses. How do you do it without downtime?

A strong answer covers:

  • Expand and contract (parallel change) approach
  • Add new column, write to both, backfill in batches
  • Switch reads to new column, then stop writing old one
  • Drop old column in a later deploy
  • Avoid long locks; test and have a rollback plan
Show model answer

A direct rename breaks old app instances still running during a rolling deploy, so I use the expand and contract pattern. Step one: add the new column as nullable, which is a fast metadata change in PostgreSQL. Step two: deploy code that writes to both the old and new columns but still reads the old one. Step three: backfill existing rows in small batches so I do not hold long locks or create huge transactions. Step four: deploy code that reads from the new column, and verify the data matches. Step five: stop writing the old column. Finally, in a later release, drop the old column. Every step is backward compatible, so I can roll back the app at any point. I also check lock timeouts and run the migration first on a staging copy of production data.

3. How do you change a public API without breaking existing clients, such as old mobile app versions?

A strong answer covers:

  • Prefer additive, backward-compatible changes
  • Version breaking changes (URL or header)
  • Deprecation policy with timelines and communication
  • Monitor usage of old versions before removing
  • Contract tests and documentation
Show model answer

Old mobile apps can stay installed for years, so I treat the API as a contract. Most changes should be additive: adding optional fields or new endpoints does not break clients that ignore unknown fields. Breaking changes, like renaming or removing a field or changing its type, go into a new version, for example /v2/orders or a version header, while v1 keeps working. I keep both versions on the same business logic with a thin mapping layer. Then I announce a deprecation timeline, add Deprecation and Sunset headers, and track how many requests still hit v1, per client version. Only when traffic is near zero, or the app forces an update, do I remove it. I use OpenAPI docs and contract tests in CI to catch accidental breaking changes before release.

4. Users say one API endpoint has become slow in production. How do you find and fix the cause?

A strong answer covers:

  • Confirm with metrics: latency percentiles, error rate, when it started
  • Use tracing to find which span is slow
  • Check recent deploys, traffic and data growth
  • Database analysis: slow query log, EXPLAIN ANALYZE, missing index, N+1
  • Fix, verify with metrics, and add alerts or tests
Show model answer

First I confirm the problem with metrics: look at p50, p95 and p99 latency for that endpoint, the error rate, and exactly when it started. Then I check what changed around that time: a deploy, a traffic spike, a big data import or a dependency slowdown. Next I use distributed tracing, such as OpenTelemetry with Jaeger or Datadog, to see which part of the request takes the time: database, an external API call or our own code. If it is the database, I check the slow query log and run EXPLAIN ANALYZE to find full scans, missing indexes, N+1 patterns or lock waits. If it is CPU, I profile. After fixing, for example adding an index or batching queries, I verify the latency graph has recovered and add an alert on p95 so we catch it earlier next time.

5. Sending emails and generating PDF invoices makes your checkout API slow. How would you move this work to the background?

A strong answer covers:

  • Return quickly and enqueue a job to a queue
  • Workers process jobs separately and scale independently
  • Retries with backoff and a dead-letter queue
  • Idempotent jobs because of at-least-once delivery
  • Monitoring queue length and failures; job status for the user
Show model answer

The checkout request should only do what must be done before replying: validate, save the order and charge payment. Then it pushes a job, such as SendInvoice with the order id, to a queue like SQS, RabbitMQ or BullMQ on Redis, and returns. To avoid losing the job if the app crashes after commit, I would use an outbox table. Separate worker processes consume jobs, generate the PDF, store it in object storage and send the email. Workers scale independently from the API. Failed jobs retry with exponential backoff, and after several failures go to a dead-letter queue for investigation. Because queues usually deliver at least once, jobs must be idempotent, for example by marking the invoice as sent. I monitor queue depth, job age and failure rate, and alert when they grow.

6. How would you add rate limiting to a public API that runs on many servers?

A strong answer covers:

  • Decide what to limit by: API key, user, IP, and per endpoint cost
  • Choose an algorithm such as token bucket or sliding window
  • Shared state in Redis with atomic operations or Lua scripts
  • Return 429 with Retry-After and rate limit headers
  • Fail open or closed decision, and limits at the edge or gateway
Show model answer

First I would decide the key: usually the API key or user id, with IP limits for anonymous traffic, and stricter limits on expensive endpoints like search or login. For the algorithm I prefer a token bucket, which allows short bursts but enforces an average rate, or a sliding window counter, which avoids the double burst at fixed window edges. Because there are many servers, the counters must be shared, so I would keep them in Redis and update them atomically with a Lua script or INCR plus EXPIRE. When a client is over the limit I return 429 with a Retry-After header, and on every response headers showing the limit and remaining quota. I would also decide what happens if Redis is down; for most APIs I fail open with a local in-memory limit. Coarse limits can also live at the API gateway or CDN to stop floods early.

7. You are asked to review the security of a REST API before launch. What would you check?

A strong answer covers:

  • Authentication strength and authorization on every object (IDOR/BOLA)
  • Input validation, parameterized queries, mass assignment
  • HTTPS, secure headers, CORS and cookie settings
  • Secrets management and least-privilege access
  • Rate limiting, logging and dependency scanning
Show model answer

I would go through the OWASP API Security Top 10. The first thing is authorization on every object: can user A fetch user B's order by changing an id? Then authentication: strong password hashing, short-lived tokens, and protection against brute force on login. Next, input: every request validated with a schema, only allowlisted fields saved to prevent mass assignment, and all SQL parameterized. I would check transport and browser settings: HTTPS only, HSTS, a strict CORS allowlist, and cookies with HttpOnly, Secure and SameSite. Then secrets: none in the code or images, stored in a secret manager, with database users on least privilege. I would also confirm rate limits, safe error messages without stack traces, audit logs for sensitive actions, and dependency and container scanning in CI. Finally, any feature that fetches URLs is checked for SSRF.

8. Your service receives webhooks from a payment provider. How do you make handling them secure and reliable?

A strong answer covers:

  • Verify the signature on the raw body and check timestamps
  • Respond quickly with 2xx and process asynchronously
  • Deduplicate by event id because deliveries can repeat
  • Handle out-of-order events, for example by fetching current state
  • Monitor failures and support replay or reconciliation
Show model answer

First, security: I verify the HMAC signature over the raw request body using a constant-time comparison, and reject events with old timestamps to stop replays. Then I store the event and return 200 quickly, because providers time out and retry if the handler is slow. The real processing happens in a background worker that reads from a queue or the stored events table. Providers deliver at least once, so I deduplicate using the event id with a unique constraint, which makes processing idempotent. Events can also arrive out of order, for example "refunded" before "paid", so I either compare timestamps or versions, or fetch the latest object state from the provider API before updating. Finally, I alert when processing fails, keep failed events for replay, and run a periodic reconciliation job that compares our records with the provider's in case a webhook was missed entirely.

9. When would you choose Kafka and when RabbitMQ for messaging between services?

A strong answer covers:

  • Kafka is a durable, partitioned log that consumers read at their own offset
  • RabbitMQ is a broker that routes messages to queues and deletes them after ack
  • Kafka suits event streaming, high throughput, replay and many consumer groups
  • RabbitMQ suits task queues, flexible routing, per-message acks and priorities
  • Both give at-least-once delivery, so consumers must be idempotent
Show model answer

They model messaging differently. Kafka is a distributed append-only log split into partitions. Messages stay for a retention period, and each consumer group tracks its own offset, so many independent services can read the same events and replay history. Ordering is kept per partition, and throughput is very high. I pick Kafka for event streaming, like order events consumed by billing, analytics and search, or for change data capture. RabbitMQ is a traditional broker: producers publish to exchanges, which route messages into queues, and a message is removed once a consumer acks it. It offers flexible routing, per-message acknowledgement, delayed retries and priorities, which makes it great for work queues like sending emails or resizing images. Both are usually at-least-once, so consumers must be idempotent. For simple needs, a managed queue like SQS can be enough.

10. How would you design an API that lets users upload large files, such as videos of several gigabytes?

A strong answer covers:

  • Upload directly to object storage with presigned URLs
  • Multipart or resumable uploads for large files
  • Validate size, type and permissions; scan for malware
  • Track upload state in the database and confirm completion
  • Process asynchronously, for example transcoding via a queue
Show model answer

I would not stream gigabytes through the API servers. Instead, the client asks the API to start an upload; the API checks permissions and limits, creates a record with status "pending" in the database, and returns presigned URLs for object storage such as S3. For big files I use multipart upload, so the client sends parts in parallel and can resume after a network failure by retrying only the missing parts. When all parts are done, the client calls a complete endpoint, or a storage event notifies us, and the API verifies size and content type. Then a job goes onto a queue for malware scanning and processing like transcoding or thumbnails, and the status changes to "ready". Files are served through a CDN with signed URLs if they are private. This keeps API servers light and lets storage handle the heavy traffic.

Hard questions hard

1. Design a URL shortener like bit.ly that handles 100 million new links per month and many more redirects. Cover IDs, storage, caching and scale.

A strong answer covers:

  • Estimate scale: read-heavy, storage size, QPS
  • Short ID generation: counter with base62, or random with collision check
  • Key-value style storage, indexed by short code
  • Heavy caching and CDN for redirects; 301 vs 302 trade-off
  • Analytics asynchronously via a queue; abuse protection
Show model answer

At 100 million links a month, writes are around 40 per second on average, but redirects may be 100 times more, so the system is read-heavy. Over five years that is about 6 billion links; with around 500 bytes each, a few terabytes. For IDs, 7 base62 characters give about 3.5 trillion combinations. I would use a distributed ID generator, for example ranges of counter values handed to each server, then base62 encode, which avoids collisions. Random IDs also work with a unique constraint and retry. Storage is a simple key-value lookup by short code, so DynamoDB, Cassandra or sharded PostgreSQL fit. Redirects go through Redis and a CDN since popular links are hot. I would use 302 if we need click analytics, because 301 is cached by browsers. Click events go to Kafka and are aggregated asynchronously. I would add rate limiting and malware URL checks.

2. A product manager says each payment event must be processed exactly once. How do you achieve that with real message queues and networks?

A strong answer covers:

  • Exactly-once delivery is not possible across networks in general
  • Use at-least-once delivery plus idempotent processing for effectively-once
  • Deduplication with unique keys or processed-event table in the same transaction
  • Transactional outbox and Kafka transactions where useful
  • Handle ordering, retries and poison messages
Show model answer

Over an unreliable network you cannot guarantee a message is delivered exactly once: if an acknowledgement is lost, the sender must choose between retrying, which risks duplicates, or not, which risks loss. So we aim for effectively-once: at-least-once delivery combined with idempotent processing. Each event carries a unique id. The consumer, in the same database transaction as its business change, inserts that id into a processed_events table with a unique constraint; a duplicate fails the insert and is skipped. On the producer side, the transactional outbox ensures the event is published if and only if the business data was committed. Kafka offers idempotent producers and transactions for read-process-write inside Kafka, but side effects in external systems still need idempotency keys, for example when calling the payment provider. I also plan for ordering per key, retries and a dead-letter queue.

3. Your single PostgreSQL database is reaching its limits. How would you scale reads and then writes, and what problems does each step bring?

A strong answer covers:

  • First optimise queries, indexes, connection pooling, vertical scaling
  • Read replicas for reads, with replication lag issues
  • Caching to reduce load
  • Partitioning and sharding for writes, choosing a shard key
  • Sharding costs: cross-shard queries, transactions, rebalancing, hot shards
Show model answer

Before adding complexity, I would fix slow queries and indexes, add PgBouncer for connection pooling, and scale the machine up, because that solves many problems cheaply. For reads, I would add caching for hot data and read replicas. Replicas are asynchronous, so there is replication lag: a user may update their profile and then read old data from a replica. I handle that by routing reads after a write, or critical reads, to the primary. For writes, a single primary is the limit. I could partition large tables by time, and archive old data. If that is not enough, I would shard by a key such as tenant_id or user_id that keeps most queries on one shard and spreads load evenly. Sharding makes cross-shard joins, transactions, unique constraints and rebalancing hard, and a bad key creates hot shards, so I would consider managed distributed databases too.

4. In a microservices system, placing an order touches the Order, Payment and Inventory services, each with its own database. How do you keep data consistent?

A strong answer covers:

  • Avoid two-phase commit across services; explain why
  • Saga: sequence of local transactions with compensating actions
  • Choreography vs orchestration trade-offs
  • Reliable events via outbox; idempotent steps
  • Eventual consistency, intermediate states shown to users, timeouts
Show model answer

There is no single ACID transaction across three databases, and two-phase commit is slow, couples services and blocks when the coordinator fails, so I would use a saga. A saga is a series of local transactions, each with a compensating action. For example: Order service creates the order as PENDING, Inventory reserves stock, Payment charges the card, then the order becomes CONFIRMED. If payment fails, Inventory releases the reservation and the order becomes CANCELLED. With choreography, services react to each other events, which is simple for few steps but hard to follow. With orchestration, a central orchestrator, for example using Temporal or a state machine, tells each service what to do, which is easier to monitor. Each step publishes events via an outbox and must be idempotent. The system is eventually consistent, so the UI shows states like pending, and timeouts trigger compensation.

5. You are building a B2B SaaS backend used by many companies. How would you design multi-tenancy and make sure one tenant can never see another tenant data?

A strong answer covers:

  • Models: shared tables with tenant_id, schema per tenant, database per tenant
  • Trade-offs in cost, isolation, migrations and noisy neighbours
  • Enforce tenant scoping centrally, e.g. row-level security or a data access layer
  • Tenant in auth token, never trusted from request body
  • Tenant-aware caching, logging, testing and backups
Show model answer

There are three main models. Shared tables with a tenant_id column is cheapest and simplest to migrate, but a missing WHERE clause can leak data, and big tenants can be noisy neighbours. Schema per tenant gives more isolation but migrations across thousands of schemas are slow. Database per tenant gives the strongest isolation and easy per-tenant backups, which enterprise or compliance customers may require, but costs more to run. Many products use shared tables by default and dedicated databases for large customers. For safety, I would never rely on each developer remembering the filter. The tenant id comes from the verified auth token, not the request body, and is enforced centrally, for example with PostgreSQL row-level security using a session variable, or a repository layer that always adds it. Cache keys, file storage paths and search indexes must include the tenant, and I would add automated tests that try cross-tenant access.

6. Design a notification service that sends email, SMS and push messages for many products, up to tens of millions of messages a day.

A strong answer covers:

  • Single API that accepts requests and writes them to a durable queue
  • Per-channel workers with provider integrations and failover
  • User preferences, quiet hours, templates and deduplication
  • Retries with backoff, rate limits per provider, dead-letter handling
  • Priorities so urgent messages like OTPs are not stuck behind bulk campaigns
  • Delivery tracking and metrics
Show model answer

Products call one API with a template id, recipient and data, plus an idempotency key. The API validates the request, stores it, and publishes it to a durable queue like Kafka or SQS, so producers are never blocked by slow providers. A routing step loads user preferences and opt-outs, applies quiet hours and deduplication, renders the template, and puts the message on a per-channel queue. I keep separate high and low priority queues so a marketing blast of five million emails cannot delay one-time passwords. Channel workers call providers such as an email service, SMS gateway, APNs and FCM, respecting each provider's rate limits, retrying with backoff, failing over to a backup provider, and sending permanent failures to a dead-letter queue. Provider callbacks update delivery status in a store built for heavy writes. Metrics on queue lag, send rate and failure rate per provider drive alerts.

7. Your company wants to split a large monolith into microservices. How would you approach it, and when would you argue against it?

A strong answer covers:

  • Start from the real problem: team scaling, deploy speed or scaling hotspots
  • Find boundaries by business domain (bounded contexts) and data ownership
  • Strangler fig: extract one service at a time behind a facade or gateway
  • Each service owns its data; handle consistency with events or sagas
  • Invest in CI/CD, observability and contracts first
  • A modular monolith may be the better answer for small teams
Show model answer

First I would ask what problem we are solving: many teams blocking each other on releases, one part needing very different scaling, or just a messy codebase. Messy code is not fixed by adding a network; a modular monolith with clear internal boundaries may be enough, especially for a small team. If we go ahead, I would map business domains and data ownership to find boundaries, then use the strangler fig pattern: put a gateway in front, extract one well-bounded area like notifications or billing, route its traffic to the new service, and repeat. Each service must own its database; shared tables bring back the coupling. Cross-service consistency then needs events, an outbox, and sagas instead of transactions. Before the first extraction I would invest in CI/CD, tracing, centralised logs and API contracts, because debugging distributed failures without them is very painful.

8. Design the backend for a real-time chat app with one-to-one and group chats, online status and message history.

A strong answer covers:

  • Persistent connections with WebSockets through gateway servers
  • Routing messages between gateways with pub/sub and a presence store
  • Message storage partitioned by conversation, with ordered ids
  • Delivery guarantees: client ids for dedup, acks, sync on reconnect
  • Offline users get push notifications; large groups need fan-out strategy
Show model answer

Clients keep a WebSocket open to a fleet of stateless gateway servers behind a load balancer. A presence store in Redis maps each online user to their gateway and expires entries via heartbeats. When a user sends a message, it carries a client-generated id for deduplication. A chat service validates membership, assigns an ordered per-conversation id, and writes it to a store partitioned by conversation id, such as Cassandra, ScyllaDB or sharded PostgreSQL, then acks the sender. It publishes the message through pub/sub, for example Redis or NATS, to the gateways of online members. Offline members get a push notification. On reconnect, clients ask for everything after the last id they saw, so missed messages are never lost. For very large groups I would fan out on read instead of pushing to every member, and I would rate limit senders to prevent spam.

9. One slow downstream dependency caused your whole platform to go down. How would you handle the incident, and what would you change afterwards?

A strong answer covers:

  • Mitigate first: shed load, disable the feature, roll back, scale
  • Explain the mechanism: threads or connections exhausted by waiting on slow calls
  • Timeouts with deadlines, circuit breakers and bulkheads
  • Limit retries with backoff, jitter and retry budgets
  • Load shedding and graceful degradation
  • Blameless postmortem with action items and better alerting
Show model answer

During the incident I focus on stopping the damage, not on the root cause: turn off the feature that calls the dependency with a flag, shed non-critical traffic, or roll back if a deploy triggered it, while keeping a timeline in the incident channel. The usual mechanism is that callers wait on the slow service with long timeouts, so their threads and connection pools fill up, and then their callers do the same. Retries at every layer multiply the load. Afterwards I would add short timeouts with propagated deadlines, circuit breakers that fail fast, and bulkheads, meaning separate pools per dependency so one cannot starve the others. Retries would be limited to one layer, with backoff, jitter and a retry budget. I would add load shedding and degraded responses, such as cached data. Finally, a blameless postmortem with owned action items and alerts on dependency latency.

10. How would you design a ledger for a wallet or payments product so balances are always correct?

A strong answer covers:

  • Double-entry bookkeeping: every transfer has balanced debit and credit entries
  • Entries are immutable and append-only; corrections are new entries
  • Integer minor units with currency, never floats
  • Idempotency keys and strong transactions or row locking to avoid double spends
  • Balances derived from entries, cached with checks, and reconciled with providers
Show model answer

I would use double-entry bookkeeping. Every movement of money is a journal transaction with two or more entries, debits and credits, that must sum to zero, so money is never created or lost by a bug. Entries are append-only and never updated; a mistake is fixed with a reversing entry, which gives a full audit trail. Amounts are stored as integers in minor units, like cents, together with a currency code. Each request carries an idempotency key with a unique constraint so retries do not double charge. To stop overspending, I write the entries and update a cached balance in one database transaction, locking the account row or using a version check, and enforcing that the balance cannot go below zero. The cached balance can always be rebuilt from entries. Daily reconciliation compares the ledger with bank and provider reports.