Low-Level Design (Object-Oriented Design) Module 9
One design, six languages interview questions
Interview questions on one design, six languages: short warm-ups, then what each harder question tests, the first step of a good answer and its follow-ups.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher
When should a type in a low-level design be a Java record, and when a class?
Answered in the lesson LLD in Java 25: records, sealed types, patterns
-
Fresher to mid
What does frozen=True give a Python dataclass, and what does it not give it?
Answered in the lesson LLD in Python 3.14: dataclasses and protocols
-
Fresher to mid
What is the difference between TypeScript's private modifier and a JavaScript #private field?
TypeScript's private is checked only by the type checker. Once the types are stripped, as Node.js does when it runs a .ts file, the field is an ordinary property: it appears in JSON.stringify and Object.keys, and any code can read it with a cast or bracket notation. A #private field is part of JavaScript itself, so the engine enforces it at run time: no outside code can name it, it never appears in JSON or in the object's keys, and a method that reads it from an object its class did not create throws a TypeError. Use #private for state that must stay hidden, such as tokens or amounts that only methods may change; private hides nothing at run time.
From the lesson LLD in TypeScript with erasable syntax only
-
Fresher to mid
How does a Go type satisfy an interface, and where should the interface be declared?
Implicitly: a type satisfies an interface when it has all of the interface's methods with the right signatures, and it never names the interface. Because of that, Go declares an interface in the package that uses it, with only the methods that package calls, and the implementing package returns concrete types. A billing package that needs a fee declares a one-method FeePolicy, and any struct with that method fits, including test doubles. Small interfaces keep fakes small and let types written elsewhere satisfy them. The compiler checks a fit only where a value is assigned to the interface, so a design adds an explicit check next to each implementation.
From the lesson LLD in Go: interfaces, embedding, generics
-
Fresher to mid
What is the rule of zero in C++, and why does it matter in a low-level design?
The rule of zero says that a class should not declare a destructor, copy or move functions of its own when its members can manage their resources themselves. Hold parts by value, text in std::string, collections in standard containers and a sole heap object in std::unique_ptr, and the compiler generates correct copying, moving and destruction from the members. In a design this keeps ownership visible in the member types instead of hidden in hand-written functions, which are where double frees and leaks come from. When a class must manage a resource directly, the C++ Core Guidelines ask for all of those functions to be defined or deleted together.
From the lesson LLD in modern C++: ownership and variants
-
Fresher to mid
Rust has no inheritance. How does a Rust design share behaviour and model "is a kind of"?
Behaviour is shared through traits: a trait declares methods, may provide default implementations, and any type can implement it, including types from other crates for your own traits. Code that needs the behaviour takes a generic parameter bounded by the trait, or a trait object such as Box<dyn Trait> when the concrete type is chosen at run time. Data is shared by composition: a struct holds other structs as fields. A closed family of kinds, such as a door's states or an operation's outcomes, is an enum whose variants carry their own data, handled with a match that the compiler checks for completeness.
From the lesson LLD in Rust: traits, enums and ownership
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Mid to senior
Payment states are records that implement a sealed interface. A teammate adds PartlyRefunded, and the build now fails in nine switch expressions. Another engineer suggests adding default branches so that it compiles. What do you reply, and where, if anywhere, does a default branch belong?
- What it tests
- Reading exhaustiveness errors as a list of decisions, the cost of default branches over sealed types, and what code compiled against the old hierarchy does when it meets the new state.
- First step
- Treat the nine errors as the list of decisions the change needs: each switch answers a question, such as how much a payment settles or what a customer is told, and partial refunds change the answer. Go through them one by one and keep defaults out, because a default answers all nine silently with the old behaviour.
- Follow-ups
-
- One of the nine switches is in another team's library, which is built and released separately.
- The transition switch refuses most state and event pairs with a catch-all case. Is that wrong too?
- How do you make sure no switch mishandles PartlyRefunded once the build is green?
From the lesson LLD in Java 25: records, sealed types, patterns
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
Design the value types of a wallet in Java 25: an amount of money, a transfer between two wallets and a monthly statement that lists transfers. Which of them are records, what does each constructor check, and how do you keep the statement immutable?
- What it tests
- Choosing records for values, checks in compact constructors, money in minor units, defensive copies of collection components, and equality that stays stable.
- First step
- Start with Money: a record holding a whole number of paise in a long, never a double, whose compact constructor refuses negative amounts. A Transfer is a record of two wallet ids and a Money that refuses a zero amount and a transfer from a wallet to itself.
- Follow-ups
-
- The wallet must hold amounts in two currencies.
- Can a record whose component is a List be a key of a HashMap?
- Statements must be sorted by time and searchable by counterparty.
From the lesson LLD in Java 25: records, sealed types, patterns
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
Three teams will write payment-gateway adapters for your Python checkout service. Do you define the gateway as an abstract base class or as a Protocol, and how do you make sure every adapter really fits before it reaches production?
- What it tests
- Nominal against structural typing, which check runs at which moment, and turning a contract into tests that every implementation must pass in continuous integration.
- First step
- Separate what the adapters must share from what must be checked. Shared retry or logging code argues for a base class; adapters that should not import your package, or vendor classes you cannot change, argue for a Protocol. Then name where each check runs: at instantiation, in a type checker, or in a contract test.
- Follow-ups
-
- One adapter is a vendor class that you cannot change.
- An adapter passed an isinstance() check and failed in production with a TypeError.
- How do you run the same scenarios against every adapter?
From the lesson LLD in Python 3.14: dataclasses and protocols
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
Order is a frozen dataclass, yet support reports that a paid order's total changed after the customer edited their cart. How can that happen, and how would you redesign Order so that it cannot?
- What it tests
- Shallow immutability, defensive copies at construction, and telling a value from an entity with a lifecycle.
- First step
- Ask what the order's lines field holds. If it is the list the cart passed in, frozen=True stops reassigning the field but not appending to the list. Reproduce it with a test that edits the cart after the order exists, and watch the total move.
- Follow-ups
-
- Why can the same frozen order not be a dictionary key either?
- Should an order be a value or an entity?
- Each line also has a quantity that the kitchen can change.
From the lesson LLD in Python 3.14: dataclasses and protocols
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
Your team moves a Python 3.12 code base to 3.14, and someone opens a change that removes the quotes from every forward reference in annotations. What can break, and how do you roll the change out safely?
- What it tests
- Deferred evaluation of annotations, code and libraries that read annotations at run time, and a migration order that keeps the old version working.
- First step
- Separate code that only writes annotations from code that reads them while the program runs: dataclasses, serialisers, validation and dependency-injection libraries, and your own metaclasses. Find every reader first, and run the whole test suite on 3.14 before any quote is removed.
- Follow-ups
-
- The package must keep running on Python 3.12 for another two releases.
- A metaclass reads __annotations__ from the class namespace while the class is being created.
- Should from __future__ import annotations be removed too?
From the lesson LLD in Python 3.14: dataclasses and protocols
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A TypeScript service has 40 enums and parameter properties in most classes, and the team wants to run it directly on Node.js 24 without a build step. How do you plan the migration, and what replaces each construct?
- What it tests
- Knowing which syntax is erasable, keeping runtime behaviour identical while the syntax changes, and using the compiler to find every place that needs work.
- First step
- Turn on erasableSyntaxOnly and verbatimModuleSyntax in tsconfig while the old build still runs, so tsc lists every construct to replace as TS1294 or TS1484 errors. Then sort them: enums, parameter properties, namespaces and import aliases each have one mechanical rewrite.
- Follow-ups
-
- Some enums are numeric, and their numbers are stored in a database.
- A library you depend on exports an enum.
- How do you prove nothing changed at run time?
From the lesson LLD in TypeScript with erasable syntax only
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
In a TypeScript booking service, a ShowId was passed where a SeatId was expected, and two customers were given the same seat. How do you design the identifiers so that this cannot compile, and where do you check them at run time?
- What it tests
- Structural typing and its limits, branded or nominal-style types, and validating untyped input once at the boundary.
- First step
- Explain why it compiled: both IDs are strings, and structural typing accepts any value of the right shape. Give each ID a brand that exists only in the type, and allow only a checking constructor function to create one.
- Follow-ups
-
- IDs arrive in JSON request bodies.
- Would classes with a private field work instead of brands?
- How do you test that the wrong call stays a compile error?
From the lesson LLD in TypeScript with erasable syntax only
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A checkout holds seats, charges a card and then confirms. Every hold must end, whether the charge succeeds, is declined or throws. How do you design this in TypeScript on Node.js 24, and what can still leave a seat held?
- What it tests
- Deterministic cleanup with using and Symbol.dispose, the order of release, and failures that no in-process mechanism survives.
- First step
- Make each hold an object whose [Symbol.dispose]() releases it, and acquire holds with using declarations inside the checkout function, so every exit releases them in reverse order. Then ask what happens when the process dies between the charge and the confirmation.
- Follow-ups
-
- The release itself calls a remote service and can fail.
- The holds are taken in a loop over a list of seats.
- The card was charged, and the process crashed before the seats were confirmed.
From the lesson LLD in TypeScript with erasable syntax only
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A teammate ports a Java class hierarchy to Go: Account has Statement(), which calls Fee(), and PremiumAccount embeds Account and defines a lower Fee(). Premium customers are still charged the standard fee. Explain why, and redesign it.
- What it tests
- Method promotion and receivers in embedding, the absence of virtual dispatch, and composition with explicit strategies.
- First step
- Say what premium.Statement() really calls: it is premium.Account.Statement(), whose receiver is the inner Account, so its call to Fee() can only reach Account.Fee. Reproduce it with a test that compares the two statements.
- Follow-ups
-
- Can an interface field make the inner method call the outer one?
- There are five account types with different fees and limits.
- How would you stop the next port from making the same mistake?
From the lesson LLD in Go: interfaces, embedding, generics
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
After a refactor, every successful seat reservation in a Go service logs "reservation failed: <nil>" and is rolled back. The function returns error. What is the likely bug, how do you prove it, and how do you fix and prevent it?
- What it tests
- The two-word representation of interface values, typed nil pointers stored in error, and guarding the pattern in reviews and tests.
- First step
- An error that prints as <nil> but compares unequal to nil is an interface holding a nil pointer. Look for a variable of a concrete error type, such as var err *ReservationError, that is returned on the success path.
- Follow-ups
-
- How do you write a test that would have caught it?
- Why did the refactor introduce it?
- Should the function return *ReservationError instead of error?
From the lesson LLD in Go: interfaces, embedding, generics
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
Your team adopts Go 1.27 and wants a generic Repository[K, V] with Map, Filter and GroupBy methods, plus a Repository interface that services depend on, so they can swap in a database later. How do you design it, and what can and cannot be generic?
- What it tests
- What Go 1.27 generic methods allow, why interface methods cannot have type parameters, and separating the storage contract from conveniences.
- First step
- Split the design in two: the contract that services depend on (Get, Put, Delete with concrete parameter types) and conveniences on the in-memory type (Map, Filter, GroupBy as generic methods). Then check each method against what an interface may contain.
- Follow-ups
-
- A teammate wants Map[R any] on the interface itself.
- The code must still build with Go 1.26 for one more release.
- How do you test that the in-memory and database repositories behave the same?
From the lesson LLD in Go: interfaces, embedding, generics
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A notification service stores channels as Notifier* and deletes them through the base. Memory grows in production and some SMS batches are never sent, yet the sanitizer job on macOS reports nothing. What do you suspect, how do you confirm it, and how do you fix the design?
- What it tests
- Virtual destructors and undefined behaviour on delete through a base, the limits of sanitizers per platform, and ownership with unique_ptr.
- First step
- Check the base class first: if Notifier's destructor is not virtual, deleting an SmsNotifier through a Notifier* never runs SmsNotifier's destructor, so whatever it flushes or frees is skipped. Search the build logs for -Wdelete-non-abstract-non-virtual-dtor and -Wdelete-abstract-non-virtual-dtor warnings.
- Follow-ups
-
- Why did the sanitizers stay quiet?
- Should the base destructor always be virtual?
- Who should own the channels?
From the lesson LLD in modern C++: ownership and variants
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
In a C++ parking design, spots have states (free, reserved, occupied, out of service) and payments have methods (card, wallet, cash, bank transfer, with more planned). Which do you model with std::variant and which with virtual functions, and why?
- What it tests
- Closed against open sets, which change each representation makes cheap, exhaustiveness checks, storage and ownership costs.
- First step
- Ask what changes more often for each: new states or new operations on states, new payment methods or new operations on payments. A closed set that gains operations suits a variant; an open set that gains kinds suits a virtual interface.
- Follow-ups
-
- A new state, suspended, is added next quarter.
- Payment methods come from plug-ins that partners write.
- What does each choice cost in memory and allocations?
From the lesson LLD in modern C++: ownership and variants
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
Two senior engineers disagree about a C++23 booking engine: one wants exceptions everywhere, the other std::expected everywhere. What do you decide, where does each mechanism belong, and how do you enforce the decision?
- What it tests
- Classifying failures, the costs and guarantees of exceptions and std::expected, and making a policy that tools and reviews can check.
- First step
- Classify the failures before choosing: refusals that callers handle as part of the flow (seat taken, over limit), faults of the environment (database down), and bugs (a broken invariant). Then give each class one mechanism and say why.
- Follow-ups
-
- A third-party library throws.
- The engine has a hot path that books 50,000 seats a second.
- How do you stop callers from ignoring a std::expected?
From the lesson LLD in modern C++: ownership and variants
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A developer ports a Java parking design to Rust: tickets hold references to spots, spots to their level, and levels back to the lot. The borrow checker rejects it at every turn. How would you redesign the object graph, and what does the new design cost?
- What it tests
- Ownership and borrowing in long-lived object graphs, IDs and newtypes, and when shared ownership with Rc and Weak is justified.
- First step
- Name the owner of each object: the lot owns levels and spots, and tickets belong to the lot too. Then replace every reference between them with an ID that the owner resolves, and see which links remain that are truly shared.
- Follow-ups
-
- A ticket ID is used after the ticket was deleted.
- Why not Rc<RefCell<Spot>> everywhere?
- Two threads must park cars in the same lot.
From the lesson LLD in Rust: traits, enums and ownership
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A pricing engine in Rust applies one of several discount strategies per order, chosen from configuration that can change while the service runs. Do you use generics or trait objects, and what happens when a teammate adds a generic method to the strategy trait?
- What it tests
- Static against dynamic dispatch, dyn compatibility, and keeping a trait usable as an object as it grows.
- First step
- Ask when the strategy's type is known. Chosen from configuration at run time, it must be a trait object such as Box<dyn Discount> or Arc<dyn Discount>; fixed per build or per call site, a generic parameter is simpler and faster.
- Follow-ups
-
- The engine prices 200,000 orders a second.
- How do you keep the trait dyn compatible as it grows?
- The strategies must also be listed by name in an admin screen.
From the lesson LLD in Rust: traits, enums and ownership
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
A Rust service built its domain model from Rc<RefCell<T>> everywhere. In production it now panics with "RefCell already borrowed" when a spot change notifies an observer that reads the lot. How do you fix this incident, and how would you redesign the model?
- What it tests
- Runtime borrow checking, re-entrant calls through observers, and designs that keep borrowing rules at compile time.
- First step
- Find the borrow that is held across the call: the lot is borrowed mutably while it notifies observers, and an observer borrows the lot again. Reproduce it in a test with one observer that reads the lot during a notification.
- Follow-ups
-
- What is the smallest safe fix for tonight?
- How do observers learn about changes without calling back into the lot?
- Would Arc<Mutex<T>> have avoided the problem?
From the lesson LLD in Rust: traits, enums and ownership
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
All Low-Level Design (Object-Oriented Design) interview questions · The Low-Level Design (Object-Oriented Design) track