System Design for Java Backend Engineers
A 45-minute structure that works: clarify and estimate, data model first, then the API, then scale what the numbers say to scale — plus idempotency, the outbox pattern and talking in numbers.
On this page
System design is the round that decides senior offers, and the most common failure is not lack of knowledge — it is starting to draw boxes before establishing what the system has to do.
Key Takeaways
- Spend the first five to eight minutes on requirements and numbers. Write them down.
- Go data model first, then API, then architecture. The data model constrains everything after it.
- Estimate, then use the estimate. A number you do not act on is decoration.
- Every distributed system needs idempotency; every event-publishing service needs the outbox.
- Say the trade-off for every choice. There are no free wins, and pretending otherwise is the tell.
A 45-minute structure
1. Clarify
Ask until you can state the problem in one sentence. Useful questions, roughly in order of value:
- Who uses this, and what are the two or three core operations?
- What is the scale — users, writes per second, reads per second?
- What is the read-to-write ratio? This decides more architecture than anything else.
- How fresh must reads be? Can they be seconds stale?
- What is explicitly out of scope?
Then state it back: "So: a URL shortener, 100 million new links a year, read-heavy at roughly 100:1, redirects must be fast globally, and analytics can lag by a minute. Authentication and billing are out of scope." Getting explicit agreement on that sentence is what stops you designing the wrong thing for forty minutes.
2. Estimate
100M new links/year
-> 100M / 3.15e7 s ≈ 3 writes/s average, say 30/s at peak
Reads at 100:1 ≈ 300/s average, 3,000/s peak
Storage: 500 bytes/link x 100M = 50GB/year
-> 5 years = 250GB. One Postgres instance. No sharding needed.
Bandwidth: 3,000 x 500B ≈ 1.5 MB/s. Trivial.
Cache: the hot 20% of a year's links = 20M x 500B = 10GB.
-> fits comfortably in one Redis instance.Now use it. Thirty writes per second means a single relational database, no queue, no sharding. Three thousand reads per second with a 10GB working set means a cache in front and nothing exotic. Saying "I would not shard here — 250GB and 30 writes per second is a single Postgres instance" is a much stronger answer than a diagram with six shards, because it demonstrates that your architecture is derived rather than recalled.
Numbers worth knowing: an SSD read is ~100µs, a memory read ~100ns, a same-datacentre round trip ~500µs, a cross-continent round trip ~150ms. A single Postgres instance handles thousands of simple writes per second; Redis handles ~100,000 operations per second.
3. Data model first
CREATE TABLE links (
code VARCHAR(8) PRIMARY KEY, -- the short code IS the key
target_url TEXT NOT NULL,
owner_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ
);
CREATE INDEX idx_links_owner ON links (owner_id, created_at DESC);Doing this before the boxes forces you to answer the questions that actually matter: what is the access pattern, what is the primary key, which queries need an index, what is the cardinality. An architecture drawn before the data model is a guess.
Say which database and why. Relational for transactions, joins and constraints — which is most things. A document store when the shape varies per record. A key-value store for a pure lookup at enormous scale. Wide-column when you have time-series data with a known partition key. "Postgres, because I need a transaction across two tables and the volume does not justify anything else" is a complete answer.
4. API
POST /v1/links
Idempotency-Key: 7f3a2b91-...
{ "targetUrl": "https://...", "expiresAt": "2027-01-01T00:00:00Z" }
-> 201 { "code": "aB3xK9", "shortUrl": "https://sho.rt/aB3xK9" }
GET /aB3xK9
-> 301 Location: https://... (cacheable, and cached by browsers)
GET /v1/links?cursor=eyJpZCI6MTIzfQ&limit=50
-> 200 { "items": [...], "nextCursor": "..." }Three details that earn credit unprompted: an idempotency key on the write; cursor pagination
rather than offset, because OFFSET 100000 scans a hundred thousand rows; and a deliberate choice of
301 versus 302 — permanent is cacheable and fast, temporary lets you count every redirect.
5. Scale what the numbers say to scale
1. Index and query tuning — free, usually the biggest win
2. Cache reads — cheap, huge for read-heavy loads
3. Read replicas — moderate; introduces replication lag
4. Vertical scaling — simple, and there is a ceiling
5. Async processing (a queue) — decouples, and adds eventual consistency
6. Sharding — expensive and hard to undo. Last.Interviewers are watching for whether you reach for sharding immediately. Most systems described in an interview do not need it, and saying "I would not shard at this volume" while showing the arithmetic is a stronger signal than drawing shards.
Where does state live? Application instances should be stateless so any of them can serve any request. Sessions go to Redis, files to object storage, and anything in a local field is a bug the moment there are two instances.
Idempotency
@PostMapping("/v1/payments")
public ResponseEntity<Payment> create(
@RequestHeader("Idempotency-Key") String key,
@RequestBody PaymentRequest request) {
// Atomic insert-if-absent. A retry of the same key returns the first result.
Optional<Payment> existing = idempotency.find(key);
if (existing.isPresent()) {
return ResponseEntity.ok(existing.get());
}
Payment payment = processor.charge(request);
idempotency.store(key, payment, Duration.ofHours(24));
return ResponseEntity.status(CREATED).body(payment);
}The client retried because it did not get a response — not because the request failed. Without idempotency, a timeout on a payment call means either losing the payment or charging twice, and there is no way to tell which. Raising this before being asked is one of the clearest senior signals available in this round.
The outbox
@Transactional
public void markPaid(String orderId) {
Order order = repository.findById(orderId).orElseThrow();
order.setStatus(PAID);
// Same transaction. Both land, or neither does.
outbox.save(new OutboxEvent("OrderPaid", orderId, toJson(order)));
}Consumers must then be idempotent, because the poller guarantees at-least-once delivery — a crash after publishing but before marking published republishes the event. Saying "at-least-once, so consumers deduplicate on the event id" completes the answer.
Consistency
Rather than reciting CAP, answer the practical question: what does this system do during a network partition?
Placing an order -> strong consistency. Refuse rather than double-sell.
Order history -> read-your-writes. A user must see their own order.
Product recommendations -> eventual. Stale by a minute is fine.
Global order count -> eventual. Nobody notices a lag.Different operations in the same system get different answers, and demonstrating that is worth more than any amount of CAP terminology. A read replica gives you cheap reads and replication lag, so route "my orders" to the primary and "browse products" to a replica.
Failure
Cover four things, briefly:
What happens when each dependency fails? Timeout, circuit breaker, and a fallback or a clean error — see Cascading failure.
What is the blast radius? Bulkheads and rate limits keep one failing feature from taking the rest down.
How do you know? RED metrics per service, alerts on saturation not just errors, distributed tracing.
How do you deploy safely? Canary or blue-green, feature flags, and backward-compatible schema migrations — expand, migrate, contract.
Say the trade-off
Every choice costs something. Naming the cost is the difference between an answer and a recitation.
| Choice | Buys | Costs |
|---|---|---|
| Cache | Latency, database load | Staleness, invalidation complexity |
| Read replicas | Read capacity | Replication lag, read-your-writes problems |
| Async queue | Decoupling, spike absorption | Eventual consistency, ordering, DLQ handling |
| Sharding | Write capacity | Cross-shard queries, rebalancing, operational load |
| Microservices | Independent deploys | Network failures, distributed transactions, tracing |
"I would add a read replica for the product catalogue and accept up to a second of replication lag, but route 'my orders' to the primary so users always see their own writes" is what a senior answer sounds like: a decision, a cost, and a boundary.
The failures that lose the round
Drawing boxes before agreeing the requirements. Estimating and then ignoring the estimate. Reaching for Kafka and sharding at a thousand requests per day. Presenting a design with no downsides. Going silent while thinking — say what you are weighing. And running out of time because you spent twenty minutes on the data model; watch the clock and say "let me move on to scaling and come back if we have time".
Frequently Asked Questions
How much of a system design interview should be spent on requirements?
What is the transactional outbox pattern?
How deep should the estimation be?
Related tutorials
- Spring & Spring Boot Interview QuestionsThe Spring questions asked at every level answered with mechanisms: how auto-configuration decides, why self-invocation breaks @Transactional, proxy modes, bean scopes and testing slices.
- The Behavioural Round: STAR Stories for EngineersWhy the behavioural round is scored harder than candidates expect, the six stories that cover almost every question, how to quantify impact honestly, and surviving the follow-up questions.
- The Coding Round: Patterns That Keep Coming BackThe six patterns that cover most coding-screen questions, with Java templates, the language-specific traps that cost points, and how to talk while you code without losing your place.
- The Eight-Week Preparation PlanA week-by-week plan that fits into eight hours a week: what to cover when, how to use spaced repetition on the topics you forget, when to start applying, and how to handle the offer stage.