InterviewLegend

Low-Level Design

Signature problem
Alex Chen32 min readUpdated 309 real interview reports
In this lesson
  • Treat classes, tables, endpoints, and queries as four LLD deliverables that must agree
  • Work a bike rental shop end to end with a method that carries to parking-garage and reservation prompts
  • Handle the normalization and growth follow-ups, and defend a booking model that never double-books a bike

Understanding the round

Wayfair splits its design signal into two separate rounds, and the low-level one is the round candidates who "did LeetCode" still fail. It is a build: you take an everyday business, a bike rental shop, an automated parking garage, a ticket reservation system, and turn it into a working object model with a database behind it. The interview usually runs on the HackerRank pair-programming portal, and the deliverables are stated plainly at the top of the round. You are asked for classes with their methods and attributes, a database schema, API specs with sample requests and responses, and SQL queries that satisfy the conditions the interviewer names. Then the follow-ups arrive on normalization and on how the design holds up as the shop grows.

The reason this round sorts people is that it asks for four things that have to line up. The classes name the behavior, and the schema names how that state is stored. The API names how the outside world calls in, and the SQL names how you answer the shop's questions against the tables. A candidate who has only practiced algorithm puzzles can write a Bike class and then stall on the schema. Or they design tables that do not match the classes, or write an availability query that quietly double-books a bike. The job is keeping those four views of the same system consistent, and the domain is deliberately simple so nothing hides behind it. Everyone knows how renting a bike works, so the entire conversation is the modeling.

The Wayfair angle
Wayfair is an e-commerce company that runs on inventory, orders, and reservations against a relational store, so this round is a scaled-down version of the work. That is why it asks for a schema and SQL instead of stopping at Java classes. The interviewer wants to see whether you can carry an object model down into normalized tables and back up into queries without losing the invariants on the way. Two prompts recur most, the bike rental shop and the automated parking garage, with ticket and hotel reservation close behind. They are the same shape underneath, inventory that gets claimed for a span of time and released, so learning one carefully means you can walk into any of them. Grade yourself on whether your bike can be handed to two renters for the same window. That double-booking is the exact bug an e-commerce checkout cannot afford, and the round is set to find it.

The low-level design method

Fix the method before touching the bike shop, because the same five moves carry any prompt the round hands you. Clarify the requirements so scope is agreed and every follow-up has a home. Name the entities, the nouns that survive the description, and give each a single responsibility and the methods it owns. Design the schema those entities persist to, normalized enough that no fact is stored twice. Define the API as endpoints with real request and response bodies, so the outside world has a contract. And write the SQL that answers the shop's questions against the schema, which is where a model that looked clean either holds up or falls apart.

The order matters because each step feeds the next, and the interviewer is watching the seams between them. Entities that were modeled loosely produce a schema that cannot enforce anything. A schema that stored the same fact in two places produces SQL that can disagree with itself. An API defined before the entities settle ends up returning fields no table holds. Working the steps in order also lets you absorb the normalization and extensibility follow-ups as small edits instead of a rewrite. Staying unflustered by those follow-ups is a large part of what the round grades.

1

Clarify the requirements

Before naming a class, pin what the shop must do and what an inventory unit is. Then pin the one rule that carries the judgment: a bike is rented for a window and never to two people at once.

2

Name the entities and their methods

Underline the nouns that survive the description, bike, bike type, rental, customer, and give each a crisp responsibility and the operations it owns.

3

Design the schema

Turn the entities into normalized tables with keys and foreign keys, so each fact lives in exactly one place and the database itself can reject a double-booking.

4

Define the API

Write the endpoints as request and response bodies: quote availability, create a rental, return a bike, list a customer's rentals. The contract the classes serve.

5

Write the SQL and handle the follow-ups

Answer the shop's real questions in SQL against the schema, then take the normalization and extensibility follow-ups as contained changes, not rewrites.

Clarifying the requirements

Resist naming a class in the first sentence. A minute spent bounding the problem is what separates a design that stays coherent from one that sprawls, and the interviewer reads whether you scope before you build. State the functional surface plainly. The shop stocks bikes of a few types, a city bike, a road bike, an electric bike, each type at some quantity. A customer rents a specific bike for a window of time, from a start to an end, and returns it. The shop must answer, for a given type and window, how many bikes are free, and it must never rent the same physical bike to two customers whose windows overlap. On return it records the bike as available again and can compute the charge for the stay.

Then draw the line around what you build now and what you name and defer, because the interviewer will push on exactly this. Say that pricing starts as a flat rate per type per day and lives behind its own calculator. A weekend surcharge or a loyalty discount is then a new rule, with no change to the return path. Say that a single shop location is the starting scope and multiple branches is a named extension. Say that payment capture, damage handling, and late fees are real but out of scope for the core model, to be added as their own tables and endpoints. Naming those deferrals out loud, rather than either ignoring them or trying to build them all, is the judgment the round rewards, because it shows you know where the design is meant to grow.

  • What the round grades: four views that agree. Classes, schema, API, and SQL describing one system without contradicting each other. No load balancers and no capacity math for a traffic spike; if you are sizing servers you have walked into the wrong round. This is one system modeled cleanly and stored in a relational database.
  • An inventory unit is a physical bike: a type is not a bike. The shop owns many city bikes, but a rental claims one specific bike, not "a city bike" in the abstract. Keeping the individual bike distinct from its type is what lets availability be exact and a return free the right unit. Collapsing the two is the first modeling slip the interviewer looks for.
  • A rental is a window, not an instant: overlap is the whole problem. A bike is rented from a start time to an end time, so two rentals of the same bike conflict when their windows overlap, not merely when they are simultaneous. Availability for a window means no overlapping rental exists, which is a range question, not a boolean flag on the bike.
  • The invariant is exactly-once: never two renters, one bike, one window. The hard correctness bar is that a physical bike is booked at most once across any instant. State it before you optimize, then let both the schema and the code enforce it, because a double-booked bike is the same defect as an oversold item at checkout.

The entities and their methods

Underline the nouns that survive the requirements and the classes are already visible. A BikeType carries the shared facts of a category: its name and its daily rate, since every city bike rents for the same price. A Bike is one physical unit, carrying its identifier, the type it belongs to, and its current status, available or out for service. A Rental is the record of one booking: which bike, which customer, and the window it covers, which is all a return and a charge need. A Customer is whoever rents. And the RentalShop is the facade that owns the inventory, routes every booking and return through one discipline, and hands the pricing decision to a collaborator rather than inlining it.

The decision that keeps this clean is to separate the BikeType from the Bike, and to model a Rental as its own object rather than a status hung on the bike. Put the rate on the type and every bike of that type shares one source of truth for price, so a rate change touches one row instead of hundreds. Give each rental its own record with a start and an end, and the availability question becomes "is there a rental for this bike whose window overlaps mine." A flag on the bike could never answer that, because a flag knows only now. A Wayfair interviewer reading this model checks whether you kept the type, the unit, and the booking as three distinct things. Merge any two of them and the SQL later is either impossible or wrong.

java
// A BikeType holds the facts every bike of that category shares. Putting the
// rate here, not on each bike, gives price one source of truth.
class BikeType {
final String name; // "city", "road", "electric"
final long dailyRateCents; // money in integer minor units, never a float
}
// A Bike is ONE physical unit. It carries its type and a service status, but it
// does NOT carry "is rented": that is a question about windows, answered by the
// rentals, not a boolean that only knows the present moment.
class Bike {
final long id;
final BikeType type;
Status status; // AVAILABLE or OUT_OF_SERVICE
}
// A Rental is the record of one booking: the bike, the customer, and the window.
// Overlap between two rentals of the same bike is the whole correctness problem,
// so the window lives here as a first-class pair, not as scattered timestamps.
class Rental {
final long id;
final long bikeId;
final long customerId;
final long start; // inclusive
final long end; // exclusive: [start, end)
}
// The facade owns inventory and routes every booking through one discipline. It
// delegates pricing to a calculator so a rate change never touches the booking
// path.
class RentalShop {
Long reserve(String type, long start, long end); // rental id, or null if none free
boolean returnBike(long rentalId, long now);
int available(String type, long start, long end);
FeeCalculator fees;
}

Notice what this modeling buys before the schema exists. The rate has one home, so pricing can change without hunting through inventory. The window lives on the rental as a half-open interval, start inclusive and end exclusive. Under that convention, back-to-back rentals, one ending exactly when the next begins, never count as a conflict. And the bike's status is reserved for the real out-of-band case, a bike pulled for a flat tire, which is genuinely different from being booked for a window. Each class has one job, and the tables will fall out of these entities almost directly, because the entities were shaped with storage in mind.

The class relationships

With the entities named, the design is the wiring between them, so draw it. Read the diagram by role. Blue is the customer, the actor entering the shop from outside. Violet is the RentalShop facade that owns the discipline. Indigo is the two collaborators it drives: the availability check that decides whether a window is free, and the fee calculator that prices a returned rental. Green is the state the design owns: the bike types, the physical bikes, and the rentals that tie a bike to a customer for a window. Follow the ownership edges to see the shape. The shop owns the inventory, a bike belongs to a type, and a rental points at both the bike and the customer. The availability check and the calculator are collaborators the shop delegates to.

Loading diagram...

The database schema

Now carry the entities down into tables, because the schema is the deliverable that separates this round from a plain coding exercise. Each class becomes a table, each single-valued attribute a column, and each relationship a foreign key. The bike type is its own table so the rate is stored once. A bike references its type by id rather than repeating the type name and rate on every row. A rental references the bike and the customer by id and carries the window as two timestamp columns. The customer is its own table. This is third normal form in practice: every non-key fact depends on its table's key and nothing else, so no fact is written in two places where the two copies could drift apart.

sql
-- Each type stored ONCE, so the daily rate has a single source of truth.
CREATE TABLE bike_type (
id BIGINT PRIMARY KEY,
name VARCHAR(40) NOT NULL UNIQUE, -- 'city', 'road', 'electric'
daily_rate_cents BIGINT NOT NULL -- money as integer minor units
);
-- A physical unit references its type by id; it does NOT copy the name or rate.
CREATE TABLE bike (
id BIGINT PRIMARY KEY,
type_id BIGINT NOT NULL REFERENCES bike_type(id),
status VARCHAR(20) NOT NULL DEFAULT 'AVAILABLE' -- AVAILABLE | OUT_OF_SERVICE
);
CREATE TABLE customer (
id BIGINT PRIMARY KEY,
name VARCHAR(120) NOT NULL,
email VARCHAR(160) NOT NULL UNIQUE
);
-- A rental is one booking. The window is [start_ts, end_ts). A row exists only
-- while the booking is live or historical; returning does not delete it.
CREATE TABLE rental (
id BIGINT PRIMARY KEY,
bike_id BIGINT NOT NULL REFERENCES bike(id),
customer_id BIGINT NOT NULL REFERENCES customer(id),
start_ts TIMESTAMPTZ NOT NULL,
end_ts TIMESTAMPTZ NOT NULL,
returned_ts TIMESTAMPTZ, -- NULL until the bike comes back
CHECK (end_ts > start_ts)
);
-- The index that makes the availability query fast: look up a bike's rentals
-- and probe the window, rather than scanning the whole table.
CREATE INDEX idx_rental_bike_window ON rental (bike_id, start_ts, end_ts);

Two choices in this schema are worth defending out loud, because the interviewer will ask. First, returning a bike does not delete its rental row; it sets returned_ts. Keeping the row is what preserves history, which the shop needs for a customer's past rentals and for revenue, and deleting it would throw that away for no gain. Second, the window is stored as two columns with a check that the end is after the start. The index on bike and window turns "is this bike free" from a full-table scan into a narrow probe. The pricing follow-up leans on the same rows. The charge for a returned rental reads the type's rate through the bike, times the days from start to end. That works only because the rate was normalized onto the type table instead of copied everywhere.

The Wayfair angle
This is where Wayfair's e-commerce instinct shows in the grading. A real catalog and order system lives or dies on a normalized relational schema, so the interviewer is fluent in exactly the tradeoffs this table set embodies and will probe them. Expect the direct question: why not store the rate on the bike, or the type name on the rental. The answer that lands is that a copied fact will eventually disagree with its other copy. Reconciling two versions of a price is the kind of quiet data bug that turns into a customer-facing wrong charge. Reach for normalization by default and relax it only with a reason. That reads as someone who has maintained a production schema instead of only drawing one on a slide.

The API design

The API is the contract the classes serve and the schema backs, and the round asks for real request and response bodies, not just endpoint names. Keep it to the operations the shop performs. Quote availability for a type and window before booking, so a customer sees what is free. Create a rental, which is the write that must respect the no-double-booking invariant. Return a bike, which closes the rental and prices it. And list a customer's rentals, which reads straight off the foreign key. Each endpoint names the fields it takes and the fields it returns, so the shape of the JSON is fixed and every field traces to a column or a computed value.

The interface
Request
how many bikes of a type are free for a window
GET /v1/availability?type=city&start=...&end=...
{ type: "city", start, end, available: 3, dailyRateCents: 1500 }
read-only; runs the overlap query against the rentals of that type
book one free bike for a window
POST /v1/rentals { customerId, type, start, end }
201 { rentalId, bikeId, start, end } or 409 if none free
claims a specific bike atomically; 409 Conflict when the window is full
return a bike and price the stay
POST /v1/rentals/{id}/return { returnedAt }
200 { rentalId, days, chargeCents } or 404 if unknown
sets returned_ts and asks the fee calculator for the charge
a customer's rental history
GET /v1/customers/{id}/rentals
{ rentals: [ { rentalId, bikeId, type, start, end, chargeCents } ] }
a plain foreign-key lookup, ordered by start time

Two things about this contract are worth naming to the interviewer. When no bike is free for the window, the create endpoint returns 409 Conflict instead of a silent empty result. The caller needs to tell "your request was fine but nothing is available" apart from "your request was malformed." And the create endpoint takes a type instead of a bike id. The customer asks for "a city bike," and the server picks which physical unit. That keeps availability and allocation the server's job, so the client never guesses at inventory. The return endpoint is the one place price is computed, and it returns the charge in the response so the client never recomputes money on its own.

The SQL that answers the shop's questions

The SQL is where the schema proves itself, because the availability question is the one the whole design exists to answer, and it is a range query, not a lookup. A bike of a requested type is free for a window when no rental of that bike overlaps the window. Two half-open windows overlap when each starts before the other ends, so the overlap test is start < :end AND end > :start. Availability for a type is then the count of bikes of that type with no overlapping rental. That is cleanest written as a bike whose id is not in the set of bikes with a conflicting rental.

sql
-- Availability: bikes of a type with NO rental overlapping [:start, :end).
-- Overlap is start < :end AND end > :start; back-to-back rentals do not clash
-- because the window is half-open.
SELECT COUNT(*) AS available
FROM bike b
JOIN bike_type t ON t.id = b.type_id
WHERE t.name = :type
AND b.status = 'AVAILABLE'
AND b.id NOT IN (
SELECT r.bike_id
FROM rental r
WHERE r.bike_id = b.id
AND r.start_ts < :end
AND r.end_ts > :start
);
-- Pick one specific free bike to book (the allocation the POST /rentals makes).
-- ORDER BY + LIMIT 1 makes the choice deterministic; FOR UPDATE holds the row so
-- two concurrent bookings cannot claim the same bike.
SELECT b.id
FROM bike b
JOIN bike_type t ON t.id = b.type_id
WHERE t.name = :type
AND b.status = 'AVAILABLE'
AND NOT EXISTS (
SELECT 1 FROM rental r
WHERE r.bike_id = b.id
AND r.start_ts < :end
AND r.end_ts > :start
)
ORDER BY b.id
LIMIT 1
FOR UPDATE;
-- Price a returned rental: the type's daily rate times the days rented, read
-- through the bike so the rate is never duplicated on the rental.
SELECT r.id,
CEIL(EXTRACT(EPOCH FROM (r.end_ts - r.start_ts)) / 86400.0) AS days,
CEIL(EXTRACT(EPOCH FROM (r.end_ts - r.start_ts)) / 86400.0) * t.daily_rate_cents AS charge_cents
FROM rental r
JOIN bike b ON b.id = r.bike_id
JOIN bike_type t ON t.id = b.type_id
WHERE r.id = :rentalId;

The allocation query is where the exactly-once invariant is enforced, so walk it carefully. Finding a free bike and then inserting its rental is two steps. If two bookings run at once, both can find the same free bike before either inserts, and the bike is booked twice. The fix is to select the candidate bike FOR UPDATE inside the same transaction as the insert. That holds a lock on the bike row, so the second booking waits, re-checks, and finds it taken. State that out loud: the availability count can be a read, but the booking must select-and-insert atomically, or the invariant is a suggestion rather than a guarantee. This is the SQL equivalent of the object model's claim-before-you-hand-out rule.

If the interviewer pushes on making the database itself refuse a double-booking, name the exclusion constraint. Postgres can enforce directly that no two rows in the rental table have the same bike and overlapping time ranges, using an exclusion constraint over a range type. A conflicting insert then fails at the database, without relying on the application to lock and check. That is the strongest version of the invariant: the guarantee lives in the schema, not in every code path that writes a rental. You do not have to write the constraint from memory. Knowing it exists, and that it is the relational answer to "prove two renters can never get one bike," is the fluency the round is set to find.

Normalization and extensibility follow-ups

The round almost always closes on two follow-ups, and having crisp answers ready is what separates a pass from a strong pass. The first is on normalization: the interviewer points at a fact and asks whether it should be stored where it is, or copied for speed. The second is on extensibility: the shop grows, inventory multiplies, traffic climbs, and the question is what in your design bends and what breaks. Neither is a trick. Both are checking whether you understand the reasons behind the choices you made, rather than having pattern-matched a template.

On normalization, hold the default and know when to relax it. The rate belongs on the type and the type name belongs on the type, so nothing is duplicated and nothing can drift; that is the answer for the core schema. But be ready to name the one honest exception: on a returned rental you may want to store the charge, and even the rate that was in effect, on the rental row itself. That looks like duplication, but it is not, because a price can change over time and a historical rental must remember what it cost, not what the type costs today. Denormalizing a point-in-time fact onto the record that needs to freeze it is correct; denormalizing a live fact that should stay single-sourced is the mistake. Drawing that line is exactly the judgment the follow-up probes.

On extensibility, walk the axes the shop grows along and show each is an addition rather than a rewrite. More bike types is new rows in the type table, nothing structural. Multiple branches is a location table and a location id on the bike, so availability scopes to a store; the queries gain a filter, not a redesign. The index already on bike and window answers heavier read traffic on availability. If quotes dominate, add a short-lived cache of availability by type and window, since the answer changes only when a booking is made. Heavier write traffic on booking is where the per-bike row lock matters, because it confines contention to the specific bike being claimed rather than serializing the whole shop. The point to make is that the object model's seams, the availability check and the fee calculator, and the schema's foreign keys are what make all of this bolt-on work.

The Wayfair angle
Wayfair layers observability questions onto the design round, so expect "how would you know this is working in production" alongside the schema follow-ups. Have a plain answer. The metrics that matter for a rental system are booking success and conflict rates, availability query latency, and the rate of returns versus overdue rentals. Those numbers tell you whether inventory is being used and whether the booking path is healthy. Tie it back to the invariant: an alert on double-booked bikes, which should be exactly zero, is the production version of the correctness bar you designed for. Naming the one metric that should never move off zero is a sharper answer than listing ten dashboards, and it shows you carried the invariant all the way into operations.

The runnable core: reservations without double-booking

The interviewer often compresses the whole design into one implementable class. For a rental shop the natural core is reservation with overlap. The shop books a specific bike for a window and refuses when every bike of that type is taken across the window. It frees a bike on cancellation and reports how many bikes of a type are free for a window. It is the full design in miniature, because the entire posture, treating a rental as a window, picking a specific free unit, and keeping the overlap check exact, lives in that one class. It is where a candidate either states clean invariants or hand-waves, and on a HackerRank editor with the tests watching, the clean statement is what passes.

The problem below keeps the booking and the overlap logic and drops the pricing math, because that is what runs deterministically. Bikes come in named types at fixed quantities, a reservation names a type and a half-open window, and the shop assigns the lowest-numbered free bike of that type. The overlap check, the refusal when the window is full, and the availability count for a window are what the problem tests. The no-double-booking case plays the same role in the full design.

1

Bike rental shop: reservations without double-booking

Medium
Problem
Implement a bike rental shop that books specific bikes for time windows and never hands one bike to two overlapping reservations. Windows are half-open: [start, end), so a reservation ending at time 5 does not conflict with one starting at time 5.

- BikeRentalShop(inventory) creates the shop, where inventory maps a bike type name to how many bikes of that type the shop owns.
- reserve(type, start, end) books the lowest-index free bike of type for [start, end). Return a reservation id, a positive integer that increases by one per successful reservation starting at 1, or -1 if the type is unknown, the window is empty (start >= end), or every bike of that type overlaps the window. A failed reserve does not consume an id.
- cancel(reservationId) frees the bike held by that reservation. Return true if the reservation was open, or false if it is unknown or already cancelled.
- available(type, start, end) returns how many bikes of type are free for the whole window [start, end), i.e. have no overlapping reservation. Return 0 for an unknown type or an empty window.

Two windows overlap when start < otherEnd AND otherStart < end. A bike is free for a window when none of its reservations overlap it.

Input: BikeRentalShop({city:2, electric:1}); reserve("city",0,5); reserve("city",3,8); available("city",0,10); reserve("city",4,6); reserve("city",8,9)

Output: reserve returns 1, then 2; available("city",0,10) returns 0; reserve("city",4,6) returns -1; reserve("city",8,9) returns 3

Both city bikes get booked for windows that cover 0 to 10, so none is free across that span. A request for 4 to 6 overlaps both existing city rentals, so it is refused. But 8 to 9 overlaps only the second rental (3 to 8 ended at 8, half-open), so the first city bike is free and it books as reservation 3.

Input: BikeRentalShop({road:1}); reserve("road",0,5); reserve("road",5,10); reserve("road",4,6)

Output: returns 1, then 2, then -1

The single road bike is booked 0 to 5, then 5 to 10, which do not overlap because the window is half-open. A request for 4 to 6 overlaps both, so with only one bike it is refused.

How this uses the pattern

Track, per bike of each type, the list of windows it is reserved for. On reserve, scan the bikes of the type in index order and take the first whose windows all miss [start, end). On available, count bikes of the type with no overlapping window. On cancel, remove the window and forget the reservation.

Bike rental shop: reservations without double-booking

Loading...
Click "Run" to execute your code

Test Cases

Input: [["BikeRentalShop",[{"city":2,"electric":1}]],["available",["city",0,10]],["reserve",["city",0,5]],["reserve",["city",3,8]],["available",["city",0,10]],["reserve",["city",4,6]],["available",["city",5,8]],["reserve",["electric",0,5]],["reserve",["electric",1,2]],["cancel",[1]],["reserve",["city",0,4]]]Expected: [null,2,1,2,0,-1,1,3,-1,true,4]
Input: [["BikeRentalShop",[{"road":1}]],["reserve",["road",0,5]],["reserve",["road",5,10]],["reserve",["road",4,6]],["available",["road",5,10]],["cancel",[2]],["available",["road",5,10]]]Expected: [null,1,2,-1,0,true,1]
Input: [["BikeRentalShop",[{"city":3}]],["available",["city",0,1]],["reserve",["mountain",0,5]],["reserve",["city",2,2]],["available",["city",100,200]]]Expected: [null,3,-1,-1,3]
Input: [["BikeRentalShop",[{"e":2}]],["reserve",["e",0,10]],["reserve",["e",0,10]],["reserve",["e",0,10]],["cancel",[99]],["cancel",[1]],["reserve",["e",0,10]],["available",["e",11,12]]]Expected: [null,1,2,-1,false,true,3,2]
Input: [["BikeRentalShop",[{"city":1}]],["reserve",["city",10,20]],["reserve",["city",0,10]],["reserve",["city",20,30]],["available",["city",5,25]],["available",["city",30,40]]]Expected: [null,1,2,3,0,1]
The Wayfair angle
This is the booking core of the Wayfair LLD reduced to what runs. The interviewer is watching three things survive the tests. A rental is treated as a window, so overlap is the conflict test. A full window returns -1 instead of double-booking a bike. And the half-open convention lets back-to-back rentals share a bike. State those invariants before you code, and drive the boundary cases yourself. Try a window that touches another exactly at its end, a type that is entirely booked, and a cancel that frees precisely the window it took. A miscount here is the same class of bug that oversells an item at checkout, so the bookkeeping has to be exact.
Take it further
A natural extension puts the systems concerns back. Make reserve safe under concurrency. The find-a-free-bike and claim-it steps must be atomic, or two reservations claim one bike. In SQL that is the select-for-update-then-insert you designed; in the object model it is a lock on the specific bike. Then add pricing on return behind a fee-calculator interface so a flat daily rate can become a weekend surcharge without touching booking, computing money in integer minor units. Finally, scale the overlap check. Scanning every window per bike is fine for a shop but not for a fleet. An interval tree or a database range index with an exclusion constraint is how the same invariant holds at size.

What the round rewards

The bar on the Wayfair low-level design round is four consistent views of one system, reasoned from the ground up. You need a clean object model, a normalized schema, an honest API, and SQL that answers the shop's real questions. The no-double-booking invariant has to hold across all of them. Whichever prompt you draw, a bike rental shop, an automated parking garage, a ticket reservation system, the moves are the same. Clarify the requirements and the deferrals before you build. Name the entities and give each one job and its methods. Carry them into tables where each fact lives once, and be ready to defend the normalization and to name the one place a point-in-time fact is worth freezing. Write the API with real bodies and the SQL with an overlap test you can explain, and lock the booking path so two callers never claim one unit. Show that judgment and you read as someone who could own a piece of a real commerce system, not someone who has only passed a coding screen.

A last word on the platform and the pace. This runs on the HackerRank pair-programming portal, and the follow-ups often arrive as fresh tabs, so pace yourself to leave room for the normalization and extensibility questions rather than polishing the first class. Narrate as you go, since the round is collaborative and the interviewer is grading your reasoning as much as your artifact. And when they ask how you would know this is healthy in production, have ready the one metric that should never move off zero. Carrying the correctness bar all the way into operations separates a strong pass from a pass.

Key Takeaways

  • ✓Wayfair's low-level design round is a build with no sizing exercise, usually on the HackerRank portal. You deliver classes with methods, a normalized DB schema, API specs with real request and response bodies, and SQL that answers the shop's questions. The bar is four views of one system that agree, so work the method in order: clarify, name entities, design the schema, define the API, write the SQL.
  • ✓Model an inventory unit as distinct from its type, and a rental as a window rather than a flag. The type owns the shared rate, the bike is the physical unit a rental claims, and the rental carries a half-open window. That window makes overlap, rather than simultaneity, the conflict test, and lets back-to-back rentals share a bike.
  • ✓Normalize by default and relax with a reason. The rate lives on the type so it is stored once. The charge on a returned rental is worth freezing, though, because a historical booking must remember what it cost. The overlap SQL is start < end AND end > start, and the booking must select-for-update-then-insert, or enforce an exclusion constraint, so two callers never claim one bike.
  • ✓Have the follow-ups ready: extensibility is additions (more types, a location table, an availability cache, per-bike row locks), not rewrites, because the object model's seams and the schema's foreign keys make it bolt-on. The runnable core is reservation with overlap: the half-open test, the refusal when full, and exact bookkeeping. The double-booked-bike alert that should read zero carries the invariant into production.
Had your Wayfair interview?Tell us what they asked. Reviewed before it goes in, never with your name.
Wayfair interview guide

Know what Wayfair asks before you walk in.

19 coding · 3 system design · 24 behavioral questions

Get Premium$49 a year · Cancel anytime
Or start freeNo card.

Get their questions first.

InterviewLegend is independent and not affiliated with or endorsed by any company it covers.