You know that feeling when you're staring at an Amazon Locker system design problem in an interview, and your mind just goes blank? It's not about the complexity of the domain itself; it's about translating real-world constraints into a scalable, fault-tolerant software architecture. I've been there, bombed that, and eventually figured out what actually works. This isn't your typical "draw some boxes and arrows" guide. We're going to talk about the LLD, the low-level design, focusing on the Java specifics that differentiate a decent solution from one that screams "hire me."
The Core Challenge: Event-Driven Locker Management
Forget the fancy UI for a minute. At its heart, an Amazon Locker system is a distributed state machine for parcels and lockers. Orders come in, parcels get assigned, lockers get opened, stuff gets picked up. Every one of those is an event, and your system needs to react to them reliably. The biggest mistake I see folks make is trying to build a monolithic CRUD app for this. You won't scale past the first holiday rush, I promise you. Think events, think queues, think idempotency.
Let's break down the primary use cases. A customer places an order, it's marked for locker delivery. A delivery driver arrives, scans the package, scans a locker, puts it in, closes it. Customer gets a notification, goes to the locker, scans a QR code, locker opens, customer retrieves package, locker closes. Each of these actions changes the state of a package, a locker, or a customer's expectation. Your system needs to reflect this in real-time, or darn close to it.
Deconstructing the LLD: Services and Data Models
For an LLD, we're not just drawing abstract services. We're talking about specific Java classes, their responsibilities, and how they interact. Think microservices, but keep them focused.
Service Decomposition (Java Packages/Modules)
You'll want to separate concerns into logical services. I'd lean towards something like this:
OrderService: Handles order creation, updates, and associating an order with a locker delivery preference. This probably lives upstream, but our system needs to integrate with it.LockerAssignmentService: The brains of the operation for finding available lockers. This service takes geographical data, locker availability, and package size into account.LockerManagementService: Manages the physical lockers. It tracks their state (available, occupied, out-of-order), handles opening/closing events, and communicates with the locker hardware.NotificationService: Sends out emails, SMS, push notifications to customers and drivers.PackageTrackingService: Monitors package status from insertion to retrieval.AuthService: Handles driver and customer authentication, QR code validation, etc. (often a shared component in larger companies).
Each of these should be a Spring Boot application, really. They're independent, they can scale separately, and they communicate via asynchronous messages.
Key Data Models (Pojos and Entities)
Let's get concrete with some Java classes.
Locker
public class Locker {
private String lockerId; // Unique identifier for the locker bank
private String locationId; // e.g., "NYC-JFK-123"
private GeoLocation geoCoordinates; // Lat/Long
private String address;
private LockerStatus status; // ACTIVE, INACTIVE, MAINTENANCE
private Set<Compartment> compartments; // One-to-many relationship
private LocalDateTime lastMaintenanceDate;
// Getters and setters
}
public enum LockerStatus {
ACTIVE, INACTIVE, MAINTENANCE
}
Compartment
public class Compartment {
private String compartmentId; // Unique ID for a specific compartment within a locker bank
private String lockerId;
private CompartmentSize size; // SMALL, MEDIUM, LARGE, XLARGE
private CompartmentStatus status; // AVAILABLE, OCCUPIED, RESERVED, OUT_OF_ORDER
private String currentPackageId; // Nullable, if occupied
private LocalDateTime lastAccessedTime;
// Getters and setters
}
public enum CompartmentSize {
SMALL, MEDIUM, LARGE, XLARGE
}
public enum CompartmentStatus {
AVAILABLE, OCCUPIED, RESERVED, OUT_OF_ORDER
}
Package
public class Package {
private String packageId; // Unique ID
private String orderId;
private String customerId;
private CompartmentSize requiredSize;
private PackageStatus status; // PENDING_ASSIGNMENT, ASSIGNED_TO_COMPARTMENT, IN_TRANSIT, IN_LOCKER, PICKED_UP, RETURNED
private String assignedLockerId; // Nullable
private String assignedCompartmentId; // Nullable
private LocalDateTime deliveryAttemptTime;
private LocalDateTime pickUpCodeExpirationTime;
private String pickUpCode; // e.g., unique QR code/numeric code
// Getters and setters
}
public enum PackageStatus {
PENDING_ASSIGNMENT, ASSIGNED_TO_COMPARTMENT, IN_TRANSIT, IN_LOCKER, PICKED_UP, RETURNED
}
You'll also need Customer and Driver entities, but their core attributes are pretty standard. The complexity here lies in the relationships and state transitions.
Persistence and Data Flow: Beyond Just a Database
A single relational database for everything? Nope. Not here. You need speed, consistency for some things, and eventual consistency for others.
Database Choices
LockerManagementServiceandLockerAssignmentService: ForLockerandCompartmentdata, a relational database (PostgreSQL, MySQL) makes sense. You need transactional integrity when a compartment changes status fromAVAILABLEtoRESERVEDorOCCUPIED. JPA with Hibernate works fine here.PackageTrackingService: Could also use a relational database, but if tracking history is extensive, a NoSQL document store (MongoDB, DynamoDB) might be better for flexibility and scalability with high write volumes.- Event Store: Crucial for auditing and replayability. Think Kafka or AWS Kinesis. Every significant change in status (package inserted, package picked up, locker opened) should be published as an event.
Asynchronous Communication with Message Queues
This is where the rubber meets the road. Services should not call each other directly for state changes. Use message queues.
- Kafka/RabbitMQ/SQS: For asynchronous communication between services. When
LockerManagementServicemarks a compartmentOCCUPIED, it publishes aCompartmentOccupiedEvent.PackageTrackingServiceconsumes this, updates the package status, andNotificationServiceconsumes it to send an alert to the customer. - Idempotency: This is non-negotiable with message queues. Assume messages can be delivered more than once. Your consumers need to handle duplicate events gracefully. A common pattern is to include a unique
eventIdin your message and store processed event IDs in your database. If you see aneventIdyou've already processed, just acknowledge and ignore it.
Handling Locker Hardware Interactions: The Edge
The physical lockers are the edge devices. They're not always online, and they have limited compute. Your LockerManagementService can't directly control them in real-time with an HTTP request.
Gateway Pattern & MQTT
Instead, picture a gateway at each locker bank. This gateway talks to the individual locker compartments via a low-power protocol (like serial, or even custom wiring over a local network). The gateway itself is connected to the internet and communicates with your LockerManagementService using a lightweight messaging protocol like MQTT.
- MQTT Broker: The
LockerManagementServicepublishes commands (e.g., "Open compartment X on locker Y") to an MQTT topic. The locker bank gateway subscribes to this topic. - Message Format: Keep it simple, JSON or Protobuf.
- Acknowledgement: The gateway needs to send an ACK back to the
LockerManagementService(via another MQTT topic) confirming the action was performed or failed. This is crucial for transactional consistency. - Offline Mode: What happens if the locker bank loses internet? The gateway needs to cache commands and synchronize state when connectivity is restored. This is a common interview follow-up. You'd need a local queue on the gateway and robust error handling. This also means your backend needs to be able to handle out-of-order or delayed state updates.
Critical Flows: Driver Delivery and Customer Pickup
Let's walk through these, focusing on the LLD components.
Driver Delivering a Package
- Driver Scans Package Barcode: Driver app (mobile client) calls
AuthServiceto authenticate the driver andPackageTrackingServiceto retrieve package details and confirm it's for locker delivery. - Driver Requests Locker: Driver app calls
LockerAssignmentService.findAvailableCompartment(packageId, requiredSize, lockerBankId)to get an available compartment. This service queries the DB forLockerandCompartmentstatuses, considering location and size. - Compartment Reservation:
LockerAssignmentServicereserves a compartment (CompartmentStatus.RESERVED) in a transaction and returns its ID. This prevents race conditions where two drivers try to claim the same compartment. - Driver Scans Compartment QR: Driver app confirms the assignment.
LockerManagementServicereceives a call (or an event) toopenCompartment(compartmentId, lockerId). - Hardware Interaction:
LockerManagementServicepublishes an MQTT message to the specific locker bank's gateway. The gateway receives it, opens the compartment, and sends an ACK. - Package Insertion & Closure: Driver places package, closes compartment. The locker hardware detects closure and sends an event (via MQTT) to
LockerManagementService. - Status Update & Notification:
LockerManagementServiceupdatesCompartmentStatus.OCCUPIED, and publishesPackageInLockerEvent.PackageTrackingServiceupdatesPackageStatus.IN_LOCKER.NotificationServicesends a pickup code (QR/numeric) to the customer via email/SMS. This pickup code is generated and stored with thePackageentity, often with an expiry.
Customer Picking Up a Package
- Customer Arrives: Customer inputs pickup code or scans QR at the locker's kiosk.
- Authentication/Validation: Kiosk sends request to
AuthService.validatePickupCode(code).AuthServicechecksPackageTrackingServiceto verify the code is valid, not expired, and matches anIN_LOCKERpackage. - Open Compartment Request: If valid,
AuthServiceinstructsLockerManagementServicetoopenCompartment(compartmentId, lockerId). - Hardware Interaction: Same MQTT flow as driver delivery. Kiosk displays "Compartment opened."
- Package Retrieval & Closure: Customer takes package, closes compartment. Locker hardware detects closure.
- Status Update:
LockerManagementServiceupdatesCompartmentStatus.AVAILABLE(orOUT_OF_ORDERif a fault is detected) and publishesPackagePickedUpEvent.PackageTrackingServiceupdatesPackageStatus.PICKED_UP.
Fault Tolerance and Error Handling: The Real World
This is where many LLDs fall apart. What happens when things break?
- Idempotent Operations: We already covered this for message consumers. Crucial.
- Retries with Backoff: Any external service call (e.g., to the locker gateway, or other internal services) should have a retry mechanism with exponential backoff. Spring's
@Retryableannotation is your friend. - Dead Letter Queues (DLQ): Messages that repeatedly fail processing in a queue should go to a DLQ for manual inspection or automated reprocessing.
- Circuit Breakers: If a downstream service is failing, don't keep hammering it. Implement circuit breakers (e.g., Resilience4j, Hystrix) to fail fast and prevent cascading failures.
- Compensating Transactions: If a multi-step operation fails midway, you might need to undo previous successful steps. For instance, if a compartment is reserved but the physical locker fails to open, you need to un-reserve that compartment. Events can help here; a
LockerOpenFailedEventcould trigger aCompartmentUnreserveCommand. - Monitoring and Alerting: Prometheus/Grafana for metrics, ELK stack for logs. You need to know when things are going wrong before customers complain. Alert on failed transactions, high queue depths, and service errors.
- Heartbeats: Locker gateways should periodically send heartbeats to
LockerManagementServiceto indicate they're alive. If a heartbeat is missed, the locker bank can be markedINACTIVEorMAINTENANCE.
Scalability and Performance: Doing It Right
- Asynchronous Processing: We're already heavy on message queues. This inherently scales well.
- Database Sharding: If you have millions of lockers, you'll need to shard your
LockerandCompartmenttables. Shard bylockerIdorlocationId. - Read Replicas: For heavily read services (like
LockerAssignmentServicechecking availability), use read replicas of your relational database. - Caching: For frequently accessed static or slowly changing data (e.g., locker locations, sizes), use Redis or Memcached. Don't cache rapidly changing compartment status, though; that needs to be fresh from the DB.
- Load Balancing: Standard stuff. AWS ALB, NGINX.
- Microservice Scaling: Each Spring Boot service can be scaled horizontally by adding more instances. Containerization (Docker, Kubernetes) makes this straightforward.
Security Considerations: Don't Forget This Part
- API Security: OAuth2/JWT for authentication and authorization for all internal and external API calls. Each service should validate tokens and enforce granular permissions.
- Data Encryption: Encrypt sensitive data at rest (database, backups) and in transit (TLS/SSL for all communication).
- Hardware Security: Physical tamper detection on lockers. Secure communication channels between the locker hardware and the gateway, and between the gateway and your backend. Don't let someone spoof a "compartment opened" event.
- Rate Limiting: Protect your APIs from abuse.
- Auditing: Log all significant actions (who opened what locker, when). Essential for debugging and compliance.
The Interview Dance: What to Expect
An interviewer will likely start broad, then zoom into specific components. They'll push you on edge cases.
"What if the driver puts the wrong package in?" Your system needs an UnassignPackageFromCompartment flow, likely requiring a support agent to intervene.
"What if the customer closes the door, but the package is still inside?" Locker hardware needs to detect package presence (e.g., weight sensor, IR beam). If a package is detected after closure, the compartment status shouldn't change to AVAILABLE. It should go to ERROR or REQUIRES_INSPECTION.
"How do you handle a power outage at a locker bank?" The gateway needs battery backup, and the system needs to reconcile state upon reconnection.
The key is to think through the entire lifecycle and its failure modes. Don't just design for the happy path. Start with clear service boundaries, defined data models, and then build out the communication and error handling. Show them you can think like an architect and an implementer. You'll impress them more with a well-thought-out error recovery strategy than a perfectly drawn but naive happy-path diagram.
Ready to Ace Your Next Interview?
Practice with AI-powered mock interviews tailored to your target role and company. Start Practicing for Free | Explore Interview Prep
