Java Backend Interview Roadmap
Java Backend Interview Roadmap
Complete Roadmap: Behavioral, Code Review, Core Java, Concurrency & System Design (Flight Search /
Skyscanner-style)
2026
Round 1: Behavioral + Code Review + Core Java (Collections, Memory, OOP, SOLID)
Round 2: System Design (Flight Search System, similar to Skyscanner)
Round 3: Behavioral + Concurrency + Advanced Java
The guide is organized so you can study it top to bottom over 3-4 weeks, or jump to weak areas. Each major
topic includes:
For experienced engineers, interviewers assume you can code. What they are really testing is:
Use STAR-E: Situation, Task, Action, Result, Extension (what you’d do differently / what you learned). The
“Extension” is what separates senior candidates from junior ones — interviewers specifically probe “what
would you redesign today?”
1. Tell me about a project you owned end-to-end. What was your role?
2. Describe a technical decision you made that you later regretted. What would you do differently?
3. Tell me about a time you disagreed with your team’s technical direction. How did you handle it?
4. Describe the architecture of a system you built. Why did you choose that architecture over alternatives?
5. What alternatives did you consider for [your major project], and why did you reject them?
6. What bottlenecks did your system have, and how did you discover them?
7. If you had to redesign your current system today, what would you change and why?
8. Tell me about a production incident you were involved in. What happened and how did you respond?
9. Describe a time you caused a bug that affected production. How did you fix it and what did you learn?
10. How do you approach debugging a production issue under pressure?
11. Tell me about a time you had to make a trade-off between shipping fast and shipping correctly.
16. What is the most challenging technical concept you’ve had to learn recently?
17. Tell me about a time you were wrong about a technical assumption. How did you find out, and what did
you do?
18. What’s a recent technology or pattern you adopted, and why?
Interviewers in Round 3 specifically dig into why decisions were made. For every major project on your
resume, prepare a one-paragraph answer covering:
The constraint that drove the decision (scale, team size, deadline, existing infra)
The 2-3 alternatives you considered
The trade-off that made you pick what you picked (e.g., consistency vs. availability, cost vs. latency)
What broke or what you’d change with hindsight
Example structure: “We chose [X] because [constraint]. We also considered [Y] and [Z], but [Y] would have
required [cost] and [Z] didn’t satisfy [requirement]. In hindsight, [X] caused [issue] under [scale], and today
I’d consider [improvement].”
Senior engineers spend more time reviewing code than writing it from scratch. Interviewers want to see if
you can spot issues without being told what to look for — this mirrors real PR review.
1. Correctness
Method length & complexity: Is a method doing too many things (violating Single Responsibility)?
Can it be broken into smaller, named steps?
Magic numbers/strings: Are there hardcoded values that should be constants or config?
Duplication (DRY): Is the same logic repeated in multiple places?
Comments: Are comments explaining why (good) or restating what the code already says (bad/smell)?
Consistency: Does the code follow existing conventions in the codebase?
Single Responsibility Principle (SRP): Does each class/method have one reason to change?
Open/Closed Principle: Can new behavior be added without modifying existing code (e.g., via
interfaces/strategy pattern instead of if-else chains on type)?
Liskov Substitution: Can subclasses be used interchangeably with their parent without breaking
behavior?
Interface Segregation: Are interfaces small and focused, or are classes forced to implement methods
they don’t need?
Dependency Inversion: Does the code depend on abstractions (interfaces) rather than concrete
implementations? Is dependency injection used appropriately?
Coupling & Cohesion: Is there unnecessary coupling between unrelated modules? Are related
behaviors grouped together?
Extensibility: If a new requirement came in (e.g., new payment type, new notification channel), how
many places would need to change?
Is synchronized used correctly — locking the right object, not too broad/narrow?
Are there potential deadlocks from lock ordering?
Below is a typical “find the issues” snippet style. Practice reviewing code like this out loud, explaining issues
category by category.
1. Correctness: [Link](itemId) returns null if the item doesn’t exist → NullPointerException on auto-
unboxing to int .
2. Thread safety: HashMap is not thread-safe; if processOrder is called concurrently, there’s a race
condition between the check ( current >= qty ) and the update ( [Link] ) — classic check-then-act
bug. Should use ConcurrentHashMap with atomic operations like compute() , or synchronize the critical
section.
3. Magic number: 0.1 (10% discount) and 1000 threshold should be named constants or configurable.
4. Single Responsibility violation: the method does inventory management, pricing/discount logic, AND
output/logging — three responsibilities in one method. Should be split into InventoryService ,
PricingService , and a notification/logging concern.
5. No return value / no exception: the method returns void and uses [Link] for both
success and failure — callers can’t programmatically know the outcome. Should throw a custom
exception (e.g., InsufficientStockException ) or return a result object.
6. Extensibility: discount logic is hardcoded inline. If a new discount tier or promotional rule is added, this
method needs to be edited again — violates Open/Closed Principle. A DiscountStrategy interface would
allow new discount types to be added without modifying this class.
“Clean Code” by Robert C. Martin (Uncle Bob) — naming, functions, SOLID chapters
“Effective Java” (3rd Edition) by Joshua Bloch — Items on API design, exceptions, generics
Google Java Style Guide: [Link]
Refactoring Guru — SOLID Principles: [Link]
Encapsulation
Bundling data (fields) and behavior (methods) together, and restricting direct access to internal state via
access modifiers ( private , protected ) and exposing controlled access through getters/setters or methods.
Why it matters: Protects invariants — e.g., a BankAccount class can ensure balance never goes negative by
controlling all mutations through a withdraw() method that validates first.
Abstraction
Hiding implementation details and exposing only essential features through interfaces or abstract classes.
The caller knows what an object does, not how.
Example: List<String> list = new ArrayList<>(); — code depends on the List interface (what), not ArrayList
internals (how).
Inheritance
A mechanism where a class (subclass) acquires properties and behavior of another class (superclass). Used
to model “is-a” relationships.
Risks: Overuse leads to fragile hierarchies — changes in the parent class can unexpectedly break subclasses
(“fragile base class problem”).
Polymorphism
The ability of an object to take many forms: - Compile-time (static): Method overloading — same method
name, different parameter lists. - Runtime (dynamic): Method overriding — subclass provides specific
implementation, resolved at runtime via dynamic dispatch (vtable lookup).
Composition vs Inheritance
Inheritance (“is-a”): Car extends Vehicle — tight coupling, subclass depends on superclass
implementation.
Composition (“has-a”): Car has an Engine field — looser coupling, behavior can be swapped at runtime
(Strategy pattern).
Rule of thumb interviewers look for: “Favor composition over inheritance” — composition gives more
flexibility, avoids deep fragile hierarchies, and supports the Open/Closed Principle better.
A class should have God classes, methods doing Split into focused classes
S - Single
only one reason to validation + business logic + (e.g., Validator , Service ,
Responsibility
change persistence Repository )
Software entities
Strategy pattern,
should be open for Long if-else / switch on type that
O - Open/Closed polymorphism, plugin-
extension, closed for grows with each new feature
style interfaces
modification
Subclass throws
Subtypes must be UnsupportedOperationException for Rethink hierarchy; use
L - Liskov
substitutable for their inherited methods (e.g., Square composition or separate
Substitution
base types extends Rectangle breaking interfaces
setWidth / setHeight independence)
Clients shouldn’t be
I - Interface forced to depend on Large “fat” interfaces with many Split into smaller, role-
Segregation methods they don’t unrelated methods specific interfaces
use
High-level modules
shouldn’t depend on Constructor injection of
D - Dependency new ConcreteClass() hardcoded inside
low-level modules; interfaces (Spring
Inversion business logic
both depend on @Autowired -style DI)
abstractions
1. Compute hashCode() of key, apply hash() function (XOR of hashCode with its upper 16 bits shifted
right) to spread bits and reduce collisions.
2. index = hash & ([Link] - 1) — works because table length is always a power of 2.
3. If bucket is empty, place new Node there.
4. If bucket has entries (collision), traverse the linked list (or tree) — if key matches (via equals() ),
update value; else append.
Java 8+ improvement: If a bucket’s linked list grows beyond a threshold ( TREEIFY_THRESHOLD = 8 ), it
converts to a red-black tree for that bucket, improving worst-case lookup from O(n) to O(log n).
Resizing: When size > capacity * loadFactor (default load factor 0.75), the table doubles in size and all
entries are rehashed.
Hash Collisions
Occur when two different keys produce the same bucket index. Handled via chaining (linked list / tree per
bucket). Poor hashCode() implementations (e.g., always returning 0) cause all entries to land in one bucket —
degrading to O(n) lookups.
If two objects are equal ( equals() returns true), they must have the same hashCode() .
The reverse is not required (different objects can share a hash, causing collisions, which is fine).
Violating this contract breaks HashMap / HashSet / HashTable behavior — e.g., you put() an object as a key,
then get() with an “equal” object but it isn’t found because hashCode differs.
ConcurrentHashMap
Fastest for
1 null key, multiple null
HashMap No No order guaranteed single-
values
threaded
Legacy,
Hashtable Yes (full lock) No order No nulls rarely used
now
Preferred
for
ConcurrentHashMap Yes (fine-grained) No order No nulls
concurrent
access
Useful for
Insertion order (or access
LinkedHashMap No 1 null key LRU
order with LRU config)
caches
O(log n)
Sorted by key operations,
TreeMap No No null keys
(Comparable/Comparator) red-black
tree
ArrayList vs LinkedList
Practical takeaway: ArrayList is the default choice for almost all use cases due to better cache
performance, even for insertions, unless you specifically need Deque behavior (frequent additions/removals
at both ends).
O(1) average
HashSet No order HashMap internally
operations
O(log n) operations,
Red-black tree (TreeMap
TreeSet Sorted order supports NavigableSet
internally)
(floor, ceiling, etc.)
O(1) avg,
O(log n)
get/contains O(1)/O(n) O(n) O(log n) O(1) avg O(log n)
worst (Java
8 treeify)
add O(1) amortized O(1) O(1) avg O(log n) O(1) avg O(log n)
remove O(n) O(1)/O(n) to find O(1) avg O(log n) O(1) avg O(log n)
1. Explain how HashMap works internally, including how put() and get() resolve collisions.
2. What changed in HashMap / ConcurrentHashMap between Java 7 and Java 8?
3. Why must equals() and hashCode() be overridden together? What breaks if you only override one?
4. When would you choose LinkedList over ArrayList ? Be honest about real trade-offs.
5. How does TreeMap maintain sorted order — what’s the underlying data structure and complexity?
6. What happens during HashMap resizing? What is the load factor and why is the default 0.75?
7. How would you implement an LRU cache using Java collections? (Hint: LinkedHashMap with
removeEldestEntry override, or HashMap + doubly linked list)
8. What is the difference between Comparable and Comparator ?
9. Why is HashMap not thread-safe — what specifically can go wrong (e.g., during resize with concurrent
puts — infinite loop bug in Java 7)?
10. What’s the difference between fail-fast and fail-safe iterators? Give examples ( ArrayList iterator vs
ConcurrentHashMap iterator).
Managed by Garbage
Lifetime Tied to method call (LIFO)
Collector
Thread-safety Each thread has its own stack Shared across all threads
Key point: A local variable Person p = new Person(); — the reference p lives on the stack, but the actual
Person object lives on the heap. When the method returns, p is popped off the stack, but the object remains
What GC Does
Automatically reclaims memory occupied by objects that are no longer reachable from any GC root (active
thread stacks, static variables, JNI references).
Most JVM GCs (G1, Parallel, CMS) divide the heap into generations based on the observation that most
objects are short-lived:
Young Generation: Split into Eden (new objects allocated here) and two Survivor spaces (S0, S1).
Minor GC runs here frequently and is fast.
Old Generation (Tenured): Objects that survive multiple minor GCs get promoted here. Major/Full
GC runs here, less frequently but more expensively.
Metaspace (Java 8+, replaced PermGen): Stores class metadata, method info.
Common GC Algorithms
Minor GC vs Major/Full GC
E.3 Memory Leaks in Java (Yes, They Happen Even With GC!)
Common causes: 1. Static collections that keep growing — e.g., a static Map<String, Object> cache that’s
never evicted. 2. Unclosed resources — database connections, streams, file handles not closed (use try-
with-resources). 3. Listener/callback registrations never deregistered — the listener holds a reference to
the object, preventing GC. 4. Inner classes holding implicit reference to outer class — a non-static
inner class or anonymous class retains a reference to its enclosing instance, even if logically unrelated. 5.
ThreadLocal misuse — values not removed after use in thread pools (threads are reused, ThreadLocal
values linger). 6. String interning of large/unbounded user-generated strings — fills the string pool.
WeakHashMap , avoiding
Collected at next GC cycle if no
Weak ( WeakReference ) memory leaks in
strong references exist
caches/listeners
Practical example: WeakHashMap is often used for caches where you want entries to be automatically
removed once the key object is no longer referenced elsewhere — prevents memory leaks from “forgotten”
cache entries.
A race condition occurs when multiple threads access shared mutable state, and the outcome depends on
the timing/interleaving of their execution.
class Counter {
private int count = 0;
public void increment() {
count++; // NOT atomic! This is read-modify-write across 3 steps
}
}
count++ is actually three operations: read count , add 1, write back. If two threads interleave between read
and write, one increment can be lost.
synchronized Keyword
Method-level: public synchronized void method() — locks on this (or the Class object for static methods).
Block-level: synchronized(lockObject) { ... } — more granular, allows locking on a specific object rather
than the whole instance.
Internally uses the object’s monitor (intrinsic lock). Only one thread can hold a monitor at a time;
others block.
Reentrant: A thread holding a lock can re-acquire it (e.g., calling another synchronized method on the
same object).
Locks ( [Link] )
ReentrantLock — explicit lock with more capabilities than synchronized : - tryLock() — attempt to acquire
without blocking indefinitely (with optional timeout). - lockInterruptibly() — allows the waiting thread to be
interrupted. - Fairness option — can configure FIFO ordering of waiting threads. - Must manually unlock() in
a finally block — risk of forgetting, unlike synchronized which auto-releases.
ReadWriteLock / ReentrantReadWriteLock: - Allows multiple readers OR one writer at a time — useful
when reads vastly outnumber writes (e.g., a configuration cache).
1. Immutability: Immutable objects (all fields final , no setters, defensive copies of mutable fields) are
inherently thread-safe — no shared mutable state to corrupt. String , Integer , records (Java 16+) are
immutable.
2. Confinement / Stateless Design: Don’t share state between threads — e.g., each request gets its
own local variables (stack-confined), or use ThreadLocal for per-thread state.
4. Synchronization: Protect shared mutable state with locks when the above approaches aren’t feasible.
CopyOnWriteArrayList
Creates a new copy of the underlying array on every write ( add , remove ). Reads are lock-free and never see
partial updates. Good for read-heavy, write-rare scenarios (e.g., list of event listeners).
Creating threads is expensive (OS-level resource), and unbounded thread creation can exhaust system
resources. The Executor framework manages a pool of reusable threads.
Supports delayed/periodic
newScheduledThreadPool(n) Cron-like tasks
execution
new ThreadPoolExecutor(
corePoolSize, // threads kept alive even if idle
maximumPoolSize, // max threads allowed
keepAliveTime, // idle time before extra threads die
[Link],
workQueue, // BlockingQueue for pending tasks
rejectionHandler // what to do when pool+queue are full
);
F.5 CompletableFuture
Key Methods
Example
This pattern is directly relevant to the Flight Search system design — fetching flight info and pricing from
different services in parallel.
Java 8+: lock-free reads, CAS-based writes, synchronized only on bin heads during collisions.
Atomic compound operations: putIfAbsent , compute , computeIfAbsent , computeIfPresent , merge — essential
for avoiding race conditions without external locks.
Visibility Problem
Without proper synchronization, one thread’s writes to a variable may not be visible to another thread due to
CPU caching and compiler reordering optimizations.
volatile Keyword
Guarantees visibility: writes to a volatile variable are immediately visible to all threads (no caching in
CPU registers/local caches).
Establishes a happens-before relationship: all writes before a volatile write are visible to any thread
that reads that volatile variable after.
Does NOT guarantee atomicity for compound operations ( volatile int count; count++; is still not atomic).
happens-before Relationship
A guarantee that memory writes by one specific statement are visible to another specific statement.
Established by: - synchronized block entry/exit - volatile variable read/write - Thread start() and join() -
[Link] utilities (locks, atomic classes)
AtomicInteger , AtomicLong , AtomicReference — provide lock-free, thread-safe operations using CAS (Compare-
And-Swap) at the hardware level.
F.8 Deadlocks
Occur when two or more threads are blocked forever, each waiting for a resource held by the other.
Classic scenario: Thread A locks Resource1, then tries to lock Resource2. Thread B locks Resource2, then
tries to lock Resource1. Neither can proceed.
Prevention strategies: - Lock ordering: Always acquire locks in a consistent global order. - Lock
timeout: Use tryLock(timeout) instead of blocking indefinitely. - Avoid nested locks where possible; use
higher-level concurrency utilities.
1. What is a race condition? Give a concrete code example and explain how to fix it.
2. Explain the difference between synchronized and ReentrantLock . When would you choose one over the
other?
3. What does the volatile keyword guarantee, and what does it NOT guarantee?
4. Walk through how ConcurrentHashMap achieves thread safety without locking the entire map.
5. What is a deadlock? How do you detect and prevent it?
6. Explain CompletableFuture — how would you fetch data from two services in parallel and combine results?
7. What is the difference between Runnable and Callable ?
8. What happens if you don’t bound your thread pool’s queue in production?
9. How would you design a thread-safe cache that’s read-heavy but occasionally updated?
(CopyOnWriteArrayList / ReadWriteLock / ConcurrentHashMap)
10. Explain the happens-before relationship and why it matters.
11. What is the difference between wait()/notify() and Lock / Condition ?
12. How does the Fork/Join framework achieve parallelism, and what is “work stealing”?
13. If you revisit the OrderProcessor code review example (Part B.3) — how would you make processOrder
thread-safe?
“Java Concurrency in Practice” by Brian Goetz — the definitive book on this topic
Oracle Java Concurrency Tutorial: [Link]
Baeldung — Java Concurrency Collection: [Link]
Baeldung — CompletableFuture Guide: [Link]
Always follow this sequence — interviewers explicitly evaluate your process, not just the final diagram:
Functional Requirements
Non-Functional Requirements
High read throughput: Search is read-heavy (millions of searches vs. relatively few bookings).
Low latency: Search results should return in ~200-500ms even with multiple airline partner calls.
High availability: Search should degrade gracefully if a partner API is slow/down (partial results better
than no results).
Eventual consistency acceptable for search results (price/availability can be slightly stale), but
booking must be strongly consistent (no overbooking).
Scalability: Handle traffic spikes (holiday seasons, flash sales).
Are we aggregating flights from multiple airlines/partners (like Skyscanner/aggregator model), or is this
a single airline’s own search system?
Do we need to support booking, or just search/comparison?
What’s the expected scale — number of daily active users, searches per second?
Do we need real-time pricing, or is slightly stale pricing (cache) acceptable?
Peak QPS (search) ~30M / 86400 * peak factor (5x) ≈ 1700 QPS
Why this matters: These numbers justify your architecture choices — e.g., heavy caching, read replicas,
CDN for static data (airport/airline metadata).
Search Endpoint
GET /flights/search
Query Params:
- source (IATA code, e.g., BOM)
- destination (IATA code, e.g., BLR)
- departureDate (YYYY-MM-DD)
- returnDate (optional, for round trips)
- passengers (adults, children, infants)
- cabinClass (economy, business, etc.)
- airlines (optional filter, comma-separated)
- priceMin, priceMax (optional filter)
- maxStops (optional filter)
- sortBy (price | duration | departureTime)
- page, pageSize (pagination)
{
"searchId": "uuid-for-this-search-session",
"results": [
{
"flightId": "AI202-BOM-BLR-2026-07-01",
"airline": "Air India",
"departureTime": "2026-07-01T06:00:00+05:30",
"arrivalTime": "2026-07-01T08:00:00+05:30",
"duration": "PT2H",
"stops": 0,
"price": { "amount": 4500, "currency": "INR" },
"availableSeats": 12,
"fareClass": "ECONOMY"
}
],
"pagination": { "page": 1, "pageSize": 20, "totalResults": 134 },
"filters": { "airlines": ["Air India", "IndiGo"], "priceRange": [3000, 12000] }
}
Other Endpoints
Pagination: Offset-based ( page , pageSize ) is simple but can have consistency issues with changing
result sets; cursor-based pagination is more robust for large/dynamic datasets, but offset is usually
acceptable for flight search given bounded result sizes per route.
Idempotency: Search is naturally idempotent (GET). Booking ( POST /bookings ) needs an idempotency
key to prevent duplicate bookings on retry.
Error Handling: Use standard HTTP status codes — 400 for invalid params (e.g., invalid airport code),
503 if downstream partner systems are unavailable (with partial results returned rather than total
failure if possible).
Versioning: /v1/flights/search to allow API evolution without breaking existing clients.
Major Components
Key Entities
Schema Considerations
Flight schedules are largely static (change infrequently) — good candidate for heavy caching / read
replicas / even a search-optimized store like Elasticsearch for the search-by-route-and-date queries.
Inventory/Availability changes frequently (every booking) — needs to be in a transactional store
(relational DB like PostgreSQL/MySQL) with strong consistency guarantees, or a dedicated inventory
service.
Separate read path (search, denormalized, cached, eventually consistent) from write path (booking,
normalized, strongly consistent) — this is essentially CQRS (Command Query Responsibility
Segregation).
Indexing Strategy
Composite index on (source, destination, departureDate) — the most common search pattern.
Index on airline for filtering.
For full-text/autocomplete on airport/city names, use a search engine like Elasticsearch rather than DB
LIKE queries.
Be mindful: too many indexes slow down writes (inventory updates) — balance read optimization on
flight/fare tables vs. write optimization on inventory tables.
Seat availability changes continuously (bookings happen every second across potentially millions of seats),
but search needs to show “reasonably current” availability without hitting the live inventory system on every
search (too slow, too much load).
Approaches
1. Event-Driven Updates - When inventory changes (booking, cancellation), the Inventory Service
publishes an event (via Kafka) — SeatAvailabilityChanged . - Search cache / read replicas subscribe and update
asynchronously. - Trade-off: slight staleness (seconds) in search results — acceptable, since actual
availability is reconfirmed at booking time.
2. Partner Synchronization (for aggregator model) - Partner airlines periodically push availability
updates (via webhooks or scheduled pulls) — frequency depends on partner SLAs (could be every few
minutes for low-demand routes, near-real-time for high-demand routes). - Use tiered sync frequency:
popular routes sync more often than obscure ones.
3. Two-Phase Confirmation at Booking Time - Search results show “indicative” availability (from
cache/last sync). - When the user proceeds to book, a real-time availability check is made directly to the
airline/inventory system — this is the authoritative check. - If unavailable at this point, show
“price/availability changed” — a familiar UX pattern on real booking sites.
Solutions: - Optimistic Locking: Inventory row has a version column. Booking transaction reads version,
attempts update WHERE version = X ; if 0 rows affected (someone else updated first), retry or fail gracefully.
Good for low-contention scenarios. - Pessimistic / Distributed Locking: Acquire a lock (e.g., via Redis
SETNX with TTL, or a distributed lock manager like Redisson/Zookeeper) on the specific flight+fareclass
inventory record before decrementing seat count. Higher contention overhead but guarantees correctness. -
Database-level atomic decrement with constraint: UPDATE inventory SET available = available - 1 WHERE
flight_id = ? AND available > 0 — atomic conditional update; if 0 rows affected, no seats available. Simple and
effective for single-DB setups. - Eventual Consistency + Compensation: Allow slight overbooking, detect
via reconciliation, and compensate (refund/rebook) — used by some airlines for overbooking strategies
intentionally, but NOT something to propose for “preventing overbooking” unless discussing real airline
practices.
Best answer for interview: Combine atomic conditional DB updates (or distributed lock for cross-service
inventory) for the booking path, with eventual consistency + events for the search/display path. Be
explicit that search-time availability is “advisory” and booking-time is “authoritative.”
What to Cache
Popular routes (e.g., Mumbai-Bangalore, Delhi-Mumbai) — high read frequency, results change
relatively slowly.
Airport and airline metadata — essentially static, cache with long TTL (hours/days) or even serve
from CDN.
Search result pages — cache by (source, destination, date, filters-hash) key with short TTL (e.g., 1-5
minutes).
Real-time seat availability for high-demand/last-minute flights — staleness here directly impacts
user trust (“I searched and it showed available, but booking failed”).
Personalized pricing (if loyalty discounts, location-based pricing apply) — caching needs to be keyed
per user-segment, not globally.
TTL-based expiry: Simple, predictable, but can serve stale data briefly.
Event-based invalidation: When inventory/price changes significantly, publish an event to invalidate
relevant cache keys (e.g., all cached searches for that flight).
Write-through vs. write-around: For flight schedule data, write-through (update cache on write)
keeps cache fresh; for high-churn inventory, write-around (cache only populated on read) with short TTL
avoids cache thrashing.
Cheaper/longer cache TTL → better performance, more risk of stale availability shown to users
(mitigated by booking-time re-verification).
The interviewer wants you to articulate this explicitly: “We accept some staleness in search results
because the booking flow re-verifies availability — this lets us cache aggressively and keep search
latency low.”
Load Balancing
Layer 7 load balancer (e.g., NGINX, ALB) distributes traffic across Search Service instances.
Use consistent hashing if routing to specific cache shards based on route/date.
Horizontal Scaling
Search Service is stateless → easy to scale horizontally by adding more instances behind the load
balancer.
Partner Integration Layer can also scale horizontally; use circuit breakers (e.g., Hystrix/Resilience4j
pattern) so a slow partner doesn’t exhaust thread pools across all instances.
Read Replicas
Flight schedule/fare database: multiple read replicas to handle the heavy read load from search; writes
(schedule updates, new routes) go to primary.
Inventory database: may need fewer replicas since writes (bookings) are more frequent relative to reads
compared to schedule data, but read replicas can still serve “advisory” availability for search.
Shard by geography/region (e.g., domestic routes per country, international routes separately) —
most searches are regional.
Alternatively shard by route hash — distributes load evenly but can complicate cross-region queries
(e.g., multi-leg international itineraries).
Distributed Caching
Redis Cluster for shared cache across Search Service instances — avoids cold caches per instance and
redundant partner API calls.
CDN for static assets (airport images, airline logos, metadata).
High Availability
Multi-AZ / multi-region deployment for the Search Service and cache layer.
Graceful degradation: if partner APIs are down, return cached/internal-inventory-only results rather than
failing the entire search — communicate to user that “some results may be limited.”
1. How would you design the search API for a flight search system? What query parameters and response
structure would you use?
2. How do you handle real-time seat availability without hitting the inventory DB on every search?
3. How would you prevent overbooking when two users try to book the last seat simultaneously?
4. What would you cache, and what would you never cache, in this system? Why?
5. How would you design the database schema for flights, fares, and inventory?
6. How would you handle a partner airline’s API being slow or down during a search request?
7. How would you support filtering and sorting efficiently at scale (e.g., sort by price across 10,000
results)?
8. How would you scale this system to handle a 10x traffic spike during a holiday sale?
9. How would you design for international flights with multiple legs/connections (multi-city itineraries)?
10. How would you handle currency conversion and region-specific pricing?
11. Design a URL Shortener (focus: hashing/encoding strategy, redirect performance, analytics).
12. Design a Payment System (focus: idempotency, consistency, reconciliation, fraud checks).
13. Design a Notification Platform (focus: fan-out, multiple channels — push/email/SMS, rate limiting,
retries).
14. Design a Ride-Sharing Service (focus: geo-indexing for driver matching, real-time location updates,
surge pricing).
G.11 General System Design Principles to Articulate
Start broad, then go deep where the interviewer steers — don’t try to cover everything equally;
watch for follow-up cues.
Always state assumptions explicitly (“I’m assuming X searches/day; if it’s 100x that, here’s what I’d
change…”).
CAP theorem awareness: search = favor Availability + Partition tolerance (AP, eventual consistency
okay); booking = favor Consistency (CP, can’t overbook).
Talk about monitoring/observability briefly — metrics (latency, error rate per partner), alerting on
partner API failures.
Mention trade-offs explicitly — there’s rarely a “correct” answer, only good reasoning about trade-
offs.
Can explain HashMap internals (hashing, collisions, treeification, resizing) without notes
Can explain ConcurrentHashMap’s locking evolution (Java 7 segments vs Java 8 CAS)
Can compare ArrayList vs LinkedList with honest trade-offs
Can explain equals()/hashCode() contract and consequences of violating it
Can explain all 5 SOLID principles with a personal project example for each
Prepared STAR-E stories for: ownership, conflict, failure/incident, technical decision, mentoring
Can answer “what would you redesign today” for your top 2 projects
Do at least 2 full mock interviews (record yourself if possible)
Every day, pick ONE small Java code snippet (from open-source PRs, or write your own buggy snippet) and
review it out loud against the checklist in Part B.2: 1. Correctness issues 2. Code quality issues 3.
Design/SOLID issues 4. Concurrency issues (if applicable)
Speaking your review out loud — as if to a colleague — builds the communication muscle interviewers
specifically evaluate.
Notice the interview is interconnected: - The Round 1 code review snippet often resurfaces in Round 3
with a concurrency angle — so when reviewing code, always ask “is this thread-safe?” even in Round 1. -
System design (Round 2) draws on Collections/Concurrency knowledge — e.g., choosing ConcurrentHashMap for
a cache, or CompletableFuture for parallel partner API calls. - Behavioral questions in Round 3 often probe the
same projects discussed in Round 1 — be consistent and deepen your answers rather than repeating them
verbatim.
Think out loud — interviewers can’t read your mind; narrate your reasoning even when uncertain.
Ask clarifying questions early — especially in system design, don’t assume scale/requirements.
It’s fine to say “I’m not 100% sure, but here’s my reasoning” — better than guessing confidently
and being wrong.
Manage time — in a 60-minute round, don’t spend 40 minutes on requirements gathering for system
design; aim for ~10 minutes on requirements/API, ~20-25 on architecture/DB, ~15-20 on deep dives
(caching/availability/scaling).
Connect answers back to production experience — “In my experience with [X], we handled this
by…” adds credibility.
Topic Resource
Closing Note
This guide mirrors the exact structure of the real interview experience it was built from: behavioral depth,
practical code review, Java internals (collections/memory/concurrency), and a realistic large-scale system
design. The candidates who succeed in these interviews aren’t the ones who memorize definitions — they’re
the ones who can reason out loud about trade-offs, own their past decisions critically, and connect
Java fundamentals to real production concerns.
Good luck.