Who this is for
You’re preparing for an Amazon SDE II or SDE III loop. Your recruiter told you one round is “Coding Basics: Logical & Maintainable.” You’ve searched online and found conflicting advice — some say it’s LeetCode, others say it’s object-oriented design. This guide explains what it actually is, how it’s evaluated, and how to prepare in 2–3 weeks of focused practice.
The core insight
Among major tech companies, Amazon is one of the few that explicitly dedicates an interview round to evaluating code maintainability, extensibility, and software design skills — separate from algorithmic problem-solving. Other companies (Google, Stripe, Airbnb) evaluate code quality within their coding rounds, but Amazon carves it out as a distinct competency with its own rubric. This means you need to prepare for it differently.
The interviewer adds requirements mid-interview specifically to test whether your architecture bends or breaks. If adding a feature requires rewriting your solution, you fail. If it requires adding a class and plugging it into an existing interface, you pass.
This round exists because Amazon operates at a scale where code maintenance cost dominates — more engineering hours are spent reading, extending, and debugging code than writing it fresh. The L&M interview simulates exactly that: can you build something a stranger could extend tomorrow?
How this round works
Duration: ~35 minutes of coding after behavioral questions (total interview is 55–60 minutes).
Format: You write real, syntactically correct code in a shared editor. No pseudocode. The interviewer presents a base problem, you implement it, then they add 2–3 follow-up requirements that force your design to stretch.
The progression pattern:
- Interviewer states a simple problem (e.g., “find files by name in a directory”)
- You clarify requirements, sketch classes, confirm approach
- You implement the base case
- Interviewer adds a twist: “now also find by size,” “now combine criteria with AND/OR”
- You extend — ideally without touching your original code
This incremental-requirements pattern is the signature of L&M. It’s designed so that candidates who hardcoded the first case must rewrite, while candidates who abstracted correctly just add a new class.
What’s evaluated (the six criteria)
Amazon interviewers score across these criteria. They are listed in priority order — correctness is the foundation, everything else builds on top:
| Criterion | What “strong” looks like | What “concern” looks like |
|---|---|---|
| Correctness | Code actually works end-to-end; edge cases handled; logic is sound | Code doesn’t solve the problem; broken logic; unhandled nulls or boundaries |
| Readability | Clear naming, logical structure, easy to follow without explanation | Interviewer can’t follow the code; poor names, tangled flow |
| Complexity management | Single-responsibility methods, each testable in isolation | God methods doing 5 things; deeply nested conditionals (>3 levels) |
| Extensibility | New requirements slot in cleanly; candidate explains how future cases would also fit | Adding a requirement means rewriting; bolting on hacks with if/else |
| Design quality | Abstractions and interfaces emerge naturally from the problem; can articulate why | Repetitive if/else chains; copy-paste with slight variations; forced patterns |
| Coherent solution | End-to-end working code that meets all stated requirements | Design doesn’t actually solve the problem, or requires interviewer to fill gaps |
Important: Beautiful architecture that doesn’t produce correct output is a fail. Correctness is table stakes — the other criteria differentiate within the pool of candidates whose code works.
Bonus signal (not required): Can you articulate where this component fits in a larger system? What API would external callers use?
The two question categories
L&M questions fall into two distinct types. Both test the same criteria but through different lenses.
Category 1: Complex logic with clean structure
Problems that have inherent logical complexity but don’t require advanced algorithms — just clear decomposition.
The pattern: The problem itself is solvable with basic programming constructs (loops, conditionals, maps), but the naive solution produces unreadable spaghetti. The test is whether you can decompose the complexity into small, named, reusable pieces.
Example problems:
- Convert an integer to its English representation (“1234” → “One Thousand Two Hundred Thirty Four”)
- Roman numeral ↔ integer conversion
- Implement a calculator that parses and evaluates expressions (with operator precedence)
- Serialize/deserialize a data structure with a clean, extensible format
What bar-raising looks like: Helper methods with descriptive names (getHundredsPlace, formatThousandsGroup), data-driven lookup tables instead of switch statements, clear separation between parsing and logic.
What concern looks like: A single 80-line function with nested ifs, magic numbers, and variables named temp2.
Category 2: Library/component design (the “design-and-build” type)
Problems that model a reusable library or service component with multiple behaviors, rules, and extension points. Amazon L&M leans heavily toward library/component design rather than domain-entity modeling.
The pattern: You design a small system from scratch — classes, interfaces, relationships — then implement key methods. The interviewer’s follow-up requirements test whether your abstractions were well-chosen.
Example problems:
| Problem | Typical follow-ups |
|---|---|
File search library (Unix find) | Search by size, date, extension; combine criteria with AND/OR; recursive flag; handle symlinks |
| Vending machine | Multiple payment types; inventory tracking; change calculation; admin refill mode |
| Rate limiter | Multiple algorithms (token bucket, sliding window); per-user vs global; distributed mode |
| Logging framework | Multiple output targets; log levels; formatting; async writes |
| Set/cache with TTL | Thread safety; background purging; dependency injection for time |
| Pizza/order pricing | Full orders (pizza + drinks); deals and promotions; per-store pricing |
| Amazon locker | Package size matching; expiration; notification system |
What bar-raising looks like: Clean interface boundaries, Strategy/Specification pattern for interchangeable behaviors, new requirements satisfied by adding classes (not modifying existing ones), working client code that demonstrates usage.
What concern looks like: One class doing everything, public fields everywhere, adding requirements means adding if (type == "new_thing") in six places.
The design patterns that actually matter
You don’t need to memorize the Gang of Four catalog. Six patterns cover 90% of L&M questions:
Specification / Strategy (most important)
Encapsulate a business rule or matching criterion as an object behind an interface.
When it appears: Any time the problem has interchangeable “criteria,” “rules,” “filters,” or “policies.” This is the single most important pattern for L&M — the File Search question (Amazon’s most classic L&M problem) is essentially a Specification pattern exercise.
interface FileFilter {
boolean matches(File file);
}
class SizeFilter implements FileFilter {
private final long minBytes;
public boolean matches(File file) {
return file.size() >= minBytes;
}
}
class ExtensionFilter implements FileFilter {
private final String ext;
public boolean matches(File file) {
return file.name().endsWith("." + ext);
}
}
The power: each rule is independently testable, composable, and new rules require zero changes to existing code.
Composite (second most important)
Combine multiple specifications/strategies into compound expressions using the same interface.
When it appears: “Now find files that match criteria A AND criteria B.” “Apply deal X OR deal Y, whichever gives a better price.” This is the natural follow-up to Specification — interviewers use it to test whether your abstraction was right.
class AndFilter implements FileFilter {
private final List<FileFilter> filters;
public boolean matches(File file) {
return filters.stream().allMatch(f -> f.matches(file));
}
}
class OrFilter implements FileFilter {
private final List<FileFilter> filters;
public boolean matches(File file) {
return filters.stream().anyMatch(f -> f.matches(file));
}
}
Factory
Create objects without specifying the exact class. Useful when the interviewer’s follow-up introduces new types.
When it appears: Vending machine product creation, rate limiter algorithm selection, locker size assignment.
State
Model an object whose behavior changes based on internal state. Eliminates complex conditional logic.
When it appears: Vending machine (idle → selecting → paying → dispensing), order lifecycle, elevator direction.
Decorator
Wrap an object to add behavior without changing its interface.
When it appears: Pizza toppings adding cost, deals modifying order price, logging/caching wrapping a service.
Dependency Injection
Pass collaborators in through the constructor rather than creating them internally. This is less a “design pattern” and more a fundamental technique — but it’s tested so frequently in L&M follow-ups that it deserves its own section.
When it appears as a follow-up:
// BAD: untestable, coupled to system clock
class TTLCache {
boolean isExpired(Entry e) {
return System.currentTimeMillis() > e.expiresAt;
}
}
// GOOD: testable, injectable
class TTLCache {
private final Clock clock;
TTLCache(Clock clock) { this.clock = clock; }
boolean isExpired(Entry e) {
return clock.now() > e.expiresAt;
}
}
Common DI targets in L&M questions:
- Time — inject
Clockinstead of callingSystem.currentTimeMillis()(TTL cache, rate limiter) - Storage — inject
Storageinterface instead of hardcoding file I/O (logging framework, file search) - Configuration — inject config rather than reading from environment (rate limiter thresholds)
Why interviewers love this: It simultaneously tests testability awareness, interface design, and separation of concerns. For SDE III candidates, proposing DI unprompted is strong signal.
When NOT to use patterns
If the problem doesn’t call for it, don’t force it. A simple problem with a forced Builder pattern looks worse than clean procedural code. The interviewer is testing judgment, not pattern-spotting. If you can articulate why a pattern fits — “I’m using Specification here because the matching criteria will expand, and I don’t want callers to change when we add new filters” — that’s strong signal.
Concurrency follow-ups
Thread safety is one of the most common follow-up questions in L&M interviews, especially for SDE III candidates. Even if the original problem is single-threaded, expect the interviewer to ask:
Common questions:
- “Is this thread-safe?”
- “What state is shared between threads?”
- “What happens if multiple threads call this concurrently?”
- “Would you use locks here? What kind?”
- “Can you make reads concurrent while writes are exclusive?”
- “How would you test for race conditions?”
Where it shows up by problem:
| Problem | Concurrency angle |
|---|---|
| TTL cache | Concurrent add / contains / purge — shared data structure access |
| Rate limiter | Concurrent request counting — atomic operations vs locks |
| File search | Parallel directory traversal — thread pool + result aggregation |
| Logging framework | Concurrent writes from multiple threads — buffer management |
| Vending machine | Concurrent user sessions — state machine race conditions |
What “meets the bar” looks like:
- Identify shared mutable state
- Propose
synchronizedorReentrantReadWriteLockwith correct granularity - Mention concurrent data structures as an alternative (
ConcurrentHashMap,AtomicInteger)
What “raises the bar” looks like:
- Propose non-blocking approaches where appropriate
- Identify contention risks (“If purging blocks
add, we get the same latency problem we’re trying to avoid”) - Discuss thread pool lifecycle and resource management
- Question whether the concurrency model is per-instance or shared
For SDE III candidates, concurrency awareness is often evaluated even when the original problem statement doesn’t mention threads. Be ready to volunteer: “Should I consider thread safety for this?”
End-to-end walkthrough: File Search Library
This is the most frequently reported Amazon L&M question. Walking through it demonstrates exactly what “extensible design” means in practice.
Step 1: Base requirement
“Implement a library that finds all files under a directory whose name contains a given string.”
First instinct (what most candidates write):
class FileFinder {
List<File> find(Directory dir, String nameContains) {
List<File> results = new ArrayList<>();
for (File f : dir.listFiles()) {
if (f.isDirectory()) {
results.addAll(find((Directory) f, nameContains));
} else if (f.name().contains(nameContains)) {
results.add(f);
}
}
return results;
}
}
This works. But it hardcodes the matching criterion into the traversal. Watch what happens next.
Step 2: Follow-up — “Now also find files larger than 5 MB”
The naive extension (concern signal):
// Adding another parameter — this doesn't scale
List<File> find(Directory dir, String nameContains, Long minSize) { ... }
Every new criterion adds a parameter. By the fourth criterion you have 8 parameters, most of them null.
The refactored approach (strength signal):
interface FileFilter {
boolean matches(File file);
}
class NameFilter implements FileFilter {
private final String substring;
NameFilter(String substring) { this.substring = substring; }
public boolean matches(File file) {
return file.name().contains(substring);
}
}
class SizeFilter implements FileFilter {
private final long minBytes;
SizeFilter(long minBytes) { this.minBytes = minBytes; }
public boolean matches(File file) {
return file.size() >= minBytes;
}
}
class FileFinder {
List<File> find(Directory dir, FileFilter filter) {
List<File> results = new ArrayList<>();
for (File f : dir.listFiles()) {
if (f.isDirectory()) {
results.addAll(find((Directory) f, filter));
} else if (filter.matches(f)) {
results.add(f);
}
}
return results;
}
}
Now the traversal logic never changes again. New criteria = new FileFilter implementation. Zero modification to FileFinder.
Step 3: Follow-up — “Find files that match BOTH name AND size”
With the right abstraction, this is trivial:
class AndFilter implements FileFilter {
private final List<FileFilter> filters;
AndFilter(FileFilter... filters) { this.filters = List.of(filters); }
public boolean matches(File file) {
return filters.stream().allMatch(f -> f.matches(file));
}
}
// Usage:
finder.find(root, new AndFilter(
new NameFilter(".xml"),
new SizeFilter(5_000_000)
));
Step 4: Follow-up — “Also support OR”
class OrFilter implements FileFilter {
private final List<FileFilter> filters;
OrFilter(FileFilter... filters) { this.filters = List.of(filters); }
public boolean matches(File file) {
return filters.stream().anyMatch(f -> f.matches(file));
}
}
One new class. No existing code changed.
Step 5 (if time): Follow-up — “Handle symbolic links”
This tests edge-case thinking:
- Symlinks can create cycles → need a visited set
- Should we follow symlinks or not? → make it configurable (another filter, or a traversal option)
The final class hierarchy
FileFilter (interface)
├── NameFilter
├── SizeFilter
├── ExtensionFilter
├── DateFilter
├── AndFilter (composite)
└── OrFilter (composite)
FileFinder
└── find(Directory, FileFilter) — recursive traversal, delegates matching
Why this impresses interviewers
- Step 2 shows you can recognize the abstraction boundary after one concrete case
- Steps 3–4 show your abstraction was right — composites plug in without touching existing code
- Step 5 shows you think about production edge cases
- The entire design follows the Open/Closed Principle without ever naming it
SDE II typically reaches Step 3 with some hints. SDE III proposes the FileFilter interface before writing the first implementation and volunteers the composite idea unprompted.
Practice problems: OO design (by frequency)
Ordered by how frequently these are reported in Amazon L&M interviews. If your time is limited, work top-down.
Very common
| Problem | Core pattern | Key follow-ups |
|---|---|---|
| File search library | Specification + Composite | See walkthrough above |
| TTL cache / expiring set | Interface extraction + DI | Thread safety; background purging; inject clock for testing |
| Vending machine | State machine | Payment types; inventory; change; admin mode |
| Logging framework | Strategy + Composite | Output targets (file/console/remote); levels; formatting; async |
| Rate limiter | Strategy | Token bucket vs sliding window; per-user vs global; distributed |
Common
| Problem | Core pattern | Key follow-ups |
|---|---|---|
| Parking lot | Factory + Strategy | Vehicle types; multi-level; pricing tiers; nearest spot |
| Amazon Locker | Strategy + Factory | Size matching; expiration; notifications |
| Pizza/order pricing | Decorator + Strategy | Combos; deals/promotions; per-store pricing |
| Elevator system | State + Strategy | Direction optimization; multiple elevators; priority |
Occasionally reported
| Problem | What it tests |
|---|---|
| Splitwise / expense sharing | Entity relationships; balance calculation; settle-up algorithm |
| Chess game | Polymorphic piece hierarchy; movement validation; board state |
| Text editor with undo/redo | Command pattern; state snapshots; clean operation interface |
| Meeting scheduler | Conflict detection; room allocation; recurring events |
| Notification service | Channel abstraction (email/SMS/push); routing; templates |
| Key-value store | GET/SET/DELETE with TTL; eviction policies; layered abstractions |
| JSON parser | Recursive descent; tokenizer → parser → value separation |
How to practice these
For each problem, spend 40 minutes:
- 5 min — sketch classes and interfaces (on paper or whiteboard, not IDE)
- 25 min — implement in your interview language
- 10 min — invent a follow-up requirement and extend
Grade yourself: did the extension require modifying existing classes, or just adding new ones?
Practice problems: LeetCode
These LeetCode problems train the same decomposition muscle. The goal isn’t to solve more problems — it’s to practice writing clean code under time pressure.
Must-do (5 problems that cover both L&M categories)
| # | Problem | Why it’s essential |
|---|---|---|
| 273 | Integer to English Words | Forces recursive decomposition by magnitude; the canonical “clean helpers” problem |
| 355 | Design Twitter | Multi-entity OO design; clean feed generation from components |
| 146 | LRU Cache | Dual data structure coordination; clean get/put/eviction boundary |
| 68 | Text Justification | Multi-step formatting; forces helper extraction for padding and line-building |
| 227 | Basic Calculator II | Tokenizer + evaluator separation; operator precedence via clean stack logic |
Solve all five with one rule: no function longer than 10 lines. This forces the decomposition habit.
Optional (grouped by what they train)
Parser / expression decomposition:
| # | Problem | What it trains |
|---|---|---|
| 224 | Basic Calculator | Nested parentheses; recursive subexpression handling |
| 394 | Decode String | Nested bracket parsing; clean recursive decoder |
| 726 | Number of Atoms | Multiplier propagation through recursive descent |
| 12 | Integer to Roman | Data-driven value table instead of cascading if/else |
| 13 | Roman to Integer | Clean lookup with subtraction rule handling |
Simulation / state management:
| # | Problem | What it trains |
|---|---|---|
| 54 | Spiral Matrix | Direction state machine with boundary tracking |
| 289 | Game of Life | Separating “count neighbors” from “apply rules” |
| 735 | Asteroid Collision | Stack simulation; clean case dispatch |
| 48 | Rotate Image | Layer-by-layer with swap helpers; isolated index math |
Design-a-class:
| # | Problem | What it trains |
|---|---|---|
| 588 | Design In-Memory File System | Path parsing + tree CRUD; real-world modeling |
| 1396 | Design Underground System | State tracking across check-in/check-out |
| 1472 | Design Browser History | Forward/back state machine; cursor management |
| 2296 | Design a Text Editor | Multi-operation state with shared cursor logic |
| 348 | Design Tic-Tac-Toe | O(1) win-check via counters |
How to use this list
Start with the 5 must-do problems. If you can solve each cleanly in 30 minutes with well-named helpers, you have the right muscle. Only move to the optional list if you want more reps on a specific weakness (e.g., you struggle with parser decomposition).
The point isn’t volume — it’s refactoring quality. After each solve, ask:
- Could someone read this without comments and understand the flow?
- Is each helper reusable, or does it embed assumptions about its one caller?
- If I added a variant (new format, new rule, new entity type), where would it go?
How to prepare (the 3-week approach)
Week 1: Build the muscle
Pick 5 OO design problems from the “very common” list and implement each from scratch in your interview language. Spend 40 minutes per problem (simulating interview time pressure). After each:
- Review: could a stranger extend this without reading the full implementation?
- Test: add a requirement you didn’t plan for. How much code changes?
- Refactor: extract any method longer than 10 lines
Week 2: Practice the incremental pattern
For each problem, define 3 follow-up requirements before you start coding. Then implement the base case, pause, and extend. Grade yourself:
- Did the follow-up require rewriting, or just adding?
- Are your interface boundaries in the right place?
- Could you explain your design choice in one sentence?
Key drill: After implementing, ask yourself: “If I hand this to another engineer and say ‘add support for X,’ could they do it without reading my implementation — just the interfaces?”
Week 3: Simulate full rounds
Run 3–4 timed mock interviews (35 minutes). Use problems you haven’t seen. Practice:
- Spending the first 5 minutes on requirements and class sketching (not coding)
- Talking through decisions aloud (“I’m making this an interface because I expect more types later”)
- Handling the curveball gracefully — when a new requirement breaks your design, acknowledge it and refactor visibly
Resources
- Awesome Low-Level Design — the most comprehensive open collection of OO design problems with solutions. Work through the Amazon-tagged ones first.
- Refactoring Guru: Design Patterns — quick visual reference for pattern structure and when to use each. Covers Specification, Strategy, Composite, State, Decorator with real examples.
- Head First Design Patterns (Freeman & Robson) — the clearest pattern reference if you need to brush up. Chapters on Strategy and Composite are directly relevant.
During the interview: the 35-minute playbook
Minutes 0–5: Requirements and design. Ask clarifying questions. Sketch 3–5 classes/interfaces on screen. Name the key methods. Confirm with the interviewer: “Does this structure make sense before I start coding?”
Minutes 5–20: Implement the base case. Start with interfaces, then implementations. Name everything clearly. Keep methods short. Talk as you code — explain why you’re creating an abstraction, not just what it does.
Minutes 20–30: Handle follow-ups. When the interviewer adds requirements, pause. Say: “Let me think about where this fits in my current design.” Then either:
- Add a new class implementing an existing interface (strong signal), or
- Acknowledge the design needs adjustment, refactor, then extend (still fine — shows adaptability)
Minutes 30–35: Wrap-up. If time allows, address one or more of:
- “Here’s how I’d test this” — name 3–4 test cases including edge cases
- “Here’s how I’d diagnose issues at runtime” — logging, metrics, tracing
- “Here’s how this handles scale” — identify the bottleneck if called 10K times/second
- “Here’s what I’d inject for testability” — clock, storage, config
Common failure modes
The God Class. Everything lives in one class with 15 methods. When the follow-up arrives, there’s nowhere clean to put it. The fix: if you catch yourself adding a fourth responsibility to a class, stop and split.
Premature coding. Jumping into implementation before sketching the class relationships. The first 5 minutes of design save 15 minutes of rewriting. Force yourself to draw boxes before writing code.
Pattern museum. Using Singleton + Factory + Builder + Observer for a vending machine. The interviewer reads this as “memorized patterns, doesn’t know when they apply.” Use only what the problem demands.
The rewrite panic. When a follow-up doesn’t fit your design, some candidates freeze or start over. Instead: acknowledge the gap, identify the minimal refactor, execute it. Interviewers want to see you adapt — that’s the whole point of the round.
Silent coding. The interviewer can’t score what they can’t observe. If you’re not talking, they’re guessing. Narrate your design decisions, especially the “why.” You can be brief — “I’m extracting this because the follow-up will probably need another variant” — but don’t go silent.
Pseudo-code trap. Amazon explicitly requires syntactically correct code. Practice in your actual interview language until standard library calls are automatic. If you forget a method name, make one up with clear semantics and move on — but the structure must be real code.
Correct-but-fragile. Code works for the stated requirements but can’t absorb any change. This is the inverse of the first failure mode — correctness without extensibility. Both matter; neither alone is sufficient.
What separates levels
SDE II:
- Implements the base case with clean structure without prompting
- Recognizes the need for abstraction after implementing one concrete case (sees the pattern after the fact)
- Extends the design for follow-ups with minimal hints
- Can discuss testing approach when asked
- Handles basic concurrency when prompted
SDE III:
- Designs the abstraction before implementing the first case (anticipates extensibility)
- Identifies composite/combinator needs without prompting (“Should I also handle combining these filters?”)
- Proposes dependency injection for testability unprompted
- Handles edge cases proactively (symlinks in file search, thread safety in TTL cache)
- Can articulate broader architectural context (“This library would sit behind an API gateway; here’s the interface external callers would use”)
- Volunteers concurrency considerations even when the problem is stated as single-threaded
The gap isn’t knowledge — it’s timing. SDE II sees the abstraction after the second case. SDE III sees it before the first.
The code review variant
Some L&M interviews include a code review segment: the interviewer shows you buggy or poorly structured code and asks for feedback. This tests the same criteria from a different angle.
What to look for (in priority order):
- Correctness bugs — race conditions, off-by-one, null handling, exception misuse
- Interface problems — should this method be public? Is the caller forced to know implementation details?
- Structural issues — methods doing too much, inconsistent abstraction levels, naming that misleads
- Testability — can this be unit tested? What would you inject?
- Style — formatting, naming conventions, magic numbers
How to deliver feedback:
- Start with the most impactful issue, not the first line you notice
- Explain why it’s a problem, not just that it is (“this
synchronizedblock doesn’t protect thecontainsmethod, so concurrent callers can get stale data”) - Propose a fix, not just a complaint
- For style issues, advocate for automated tooling rather than manual policing
What to do next
- Work through the file search library walkthrough above end-to-end in your interview language. Time yourself — can you reach Step 4 in 25 minutes?
- Solve LC 273 (Integer to English Words) with the “no function longer than 10 lines” rule.
- Practice the TTL cache problem with thread safety as the follow-up — this combines OO design with concurrency.
- Review your interview language’s concurrency primitives (
synchronized,ReentrantLock,ConcurrentHashMapin Java;threading.Lock,queue.Queuein Python). - Browse awesome-low-level-design for worked solutions to the OO design problems above.