The coding question
Design the classes for an Amazon locker system. Packages of different sizes go into lockers of different sizes, customers get a code to open theirs, and unclaimed packages get returned after a few days.
Ask a few questions first
| Ask | What we'll assume |
|---|---|
| One locker station or many? | We'll design one station. Many stations is a follow-up. |
| What sizes? | Small, medium and large. A package fits any locker its size or bigger. |
| What if nothing fits? | Return nothing, and the driver takes it to another station. |
| What's the code? | A random 6-digit code, unique among packages currently in the station. It opens one locker once. |
| How long until a return? | 3 days by default, but it should be easy to change. A daily job pulls expired packages. |
| How do we handle time? | Pass the day in, so it's easy to test. A real system would use a clock object. |
A couple of examples
| Input | Output |
|---|---|
| Lockers: A small, B large. Deliver a medium package on day 0 | It goes in B, and you get a 6-digit code |
| Then deliver another medium package | None: nothing free fits, so the driver tries another station |
| A small package goes in A on day 0 and nobody picks it up. Run the expiry check on day 3 | That package comes back for return, and A is free again |
Start with brute force
The simple version is one big class with a list of lockers and a dict of codes, and all the logic in a few long methods. It works, but it's hard to change: adding a new locker size or a new rule means editing everything.
Cost: Fine for speed, since a station has maybe a hundred lockers. The cost is in how hard it is to change.
A better way
In a low-level design round, they're grading how easy your design is to change. Start by listing the nouns: package, locker, size, code, station. Each becomes a class that owns its own data. That's the core of object-oriented design.
Each class does one job. A Locker knows if it's free and whether a package fits. A CodeGenerator makes codes, so you can swap it for a fake one in tests. The LockerStation coordinates: picks a locker, hands out codes and handles expiry.
Pick-up is a lookup by code, so keep a dict from code to locker. That's a hash map, and it makes pick-up O(1).
- Size is an enum with an order, so 'fits' is just locker size >= package size.
- Package holds an ID and a size. Locker holds its size, the package inside, the code and the day it was filled.
- To deliver, sort lockers by size and take the smallest free one that fits. That keeps big lockers open for big packages.
- Get a code from the CodeGenerator, store the package in the locker, and save code to locker in the station's dict.
- To pick up, pop the code from the dict and empty the locker. An unknown code returns nothing.
- Once a day, the expiry check empties every locker held at least hold_days and returns those packages.
The code
import random
from enum import IntEnum
class Size(IntEnum):
SMALL = 1
MEDIUM = 2
LARGE = 3
class Package:
def __init__(self, package_id, size):
self.id = package_id
self.size = size
class Locker:
def __init__(self, locker_id, size):
self.id = locker_id
self.size = size
self.package = None
self.code = None
self.placed_day = None
def is_free(self):
return self.package is None
def fits(self, package):
return self.is_free() and self.size >= package.size
def put(self, package, code, day):
self.package = package
self.code = code
self.placed_day = day
def take_out(self):
package = self.package
self.package = None
self.code = None
self.placed_day = None
return package
class CodeGenerator:
def __init__(self, seed=None):
self.rng = random.Random(seed)
def new_code(self, in_use):
while True:
code = str(self.rng.randint(0, 999999)).zfill(6)
if code not in in_use:
return code
class LockerStation:
def __init__(self, lockers, hold_days=3, codes=None):
self.lockers = sorted(lockers, key=lambda locker: locker.size)
self.hold_days = hold_days
self.codes = codes or CodeGenerator()
self.by_code = {}
def deliver(self, package, day):
# Smallest free locker that fits, so big lockers stay free for big packages.
for locker in self.lockers:
if locker.fits(package):
code = self.codes.new_code(self.by_code)
locker.put(package, code, day)
self.by_code[code] = locker
return code
return None # Station full for this size; the driver tries another one.
def pick_up(self, code):
locker = self.by_code.pop(code, None)
if locker is None:
return None
return locker.take_out()
def expired(self, day):
# Run once a day: empty lockers held too long and hand back the packages.
returns = []
for code, locker in list(self.by_code.items()):
if day - locker.placed_day >= self.hold_days:
del self.by_code[code]
returns.append(locker.take_out())
return returns| Cost | Why | |
|---|---|---|
| Deliver | O(L) | Walks the sorted lockers. L is maybe 100, so that's fine. Free lists per size would make it O(1) |
| Pick up | O(1) | One dict lookup by code |
| Expiry check | O(P) | Looks at every filled locker once a day |
| Space | O(L) | One object per locker, plus the code dict |
Test it
Walk through one of the examples out loud, then check the cases that tend to break things:
- A wrong code returns None and doesn't open anything.
- The same code used twice: the second time returns None, since pick-up removes it.
- A package picked up before expiry isn't in the dict anymore, so the expiry check skips it.
- A full station returns None from deliver instead of crashing.
- Two packages never get the same code, because new_code checks codes in use.
Common mistakes
- Putting every rule in one giant class, so adding a size or a new kind of locker means editing everything.
- Picking the first free locker that fits instead of the smallest, which wastes large lockers on small packages.
- Reading the real clock inside the class, which makes expiry hard to test.
- Forgetting to delete the code after pick-up, so an old code can open a locker after a new package goes in.
- Diving into code before agreeing on the classes. Sketch them and their methods first.
Follow-up questions
How would you support many stations?
Add a LockerService that holds stations by location. Delivery asks the nearest station first, then the next. Codes become unique per station, or include the station ID.
How do you make delivery O(1)?
Keep a list of free lockers per size. Take from the smallest size that has one, and add lockers back when they empty.
What about notifying the customer?
Have the station call a Notifier interface after delivery and before a return. Email and SMS are separate classes behind it, so adding one doesn't touch the station.
How the time adds up
| Minutes | What you're doing |
|---|---|
| 0–5 | Ask questions and pick a concrete version |
| 5–10 | List the classes and what each one does |
| 10–20 | Walk through deliver, pick up and expiry |
| 20–35 | Write the classes |
| 35–40 | Test with the examples and edge cases |
| 40–45 | Follow-up questions |
Key concepts used here
- Object-oriented designSplit a small program into classes that are easy to change.
- Hash maps and sliding windowsSwap nested loops for a single pass.
We wrote this sample solution ourselves. Practice Loop is independent and isn't affiliated with or endorsed by Amazon.