← Object Oriented Design Interview 12Design a Shipping Locker System

12Design a Shipping Locker System

In this chapter, we will design a Shipping Locker system similar to UPS, FedEx, or Amazon Locker. It offers customers a convenient and secure way to pick up their online orders. The system manages locker availability, assigns incoming packages to appropriate lockers, and ensures a smooth package retrieval process for customers. To realize this vision of a convenient and secure locker system, let’s explore what it needs to do. Image represents a simplified illustration of a self-service locker system.  A cartoon girl stands to the left, interacting with a bank of lockers depicted as a gray rectangular unit with a shelf-like top and bottom. The locker unit contains twelve individual compartments arranged in a four-by-three grid. One compartment is partially open, revealing a darker gray interior, suggesting an item is being retrieved. The girl is shown holding a similar-sized square object, implying she is either placing an item in or retrieving one from a locker. On the rightmost side of the locker unit is a small control panel labeled 'PICKUP,' featuring a gray display screen and three buttons below it, suggesting a user interface for selecting and accessing lockers. The overall style is simplistic and cartoonish, focusing on the interaction between the user and the locker system. Shipping Locker System

Requirements Gathering

The first step in designing a shipping locker system is to clarify the requirements and narrow down the scope. Here is an example of a typical prompt an interviewer might give: “Imagine you’ve just received a notification that your online order has been delivered to a shipping locker near you. You head to the locker, enter a secure access code, and instantly, the door pops open, revealing your package. Behind the scenes, the system has already managed locker availability, assigned an appropriately sized compartment, and ensured a seamless pickup experience. Now, let’s design a shipping locker system that can do all of this.”

Requirements clarification

Here is an example of how a conversation between a candidate and an interviewer might unfold: Candidate: Does the system support multiple locker sizes?
Interviewer: Yes, the system has lockers of different sizes. The design should ensure packages are assigned to the smallest available locker that fits, optimizing space use. Candidate: So, what happens when a package gets to the locker, and how does the customer end up picking it up?
Interviewer: When a package arrives, the system finds an open locker that’s the right size and assigns it there. Once the package is tucked inside, the customer gets a notification with the locker’s location and a unique access code. They just punch in the code to pop the locker open, grab their package, and that frees up the locker for the next delivery. Candidate: Is there a time limit or any fees for using the locker?
Interviewer: Good question. That’s actually a big part of how the system works. We’ve got a locker policy that gives customers a ‘Free Period’, a set number of days during which they can use the locker for free. Once that’s up, we start charging a daily fee based on the locker’s size. And if the package sits there past a ‘Maximum Period,’ which is also predefined, one of our staff members steps in, pulls it out, and clears the locker for someone else. Candidate: Since the system tracks locker usage costs through the locker policy, does it handle payments too?
Interviewer: For simplicity’s sake, we let an external service take care of the actual payment processing. That’s not something the system itself has to worry about.

Requirements

Here are the key functional requirements we’ve identified.

  • The system should keep track of all lockers and support different locker sizes.
  • The system should smartly assign lockers by matching package size to the smallest available option that fits, keeping things efficient.
  • Customers should be able to pop open their assigned locker with a unique access code.
  • The system should monitor storage costs based on the customer’s locker policy, factoring in the daily rate tied to that specific locker size.

Below are the non-functional requirements:

  • The system should handle a high volume of locker operations per site without performance degradation, accommodating busy locations.
  • The system must maintain high availability, ensuring lockers are accessible to customers and staff at all times.

Now that we’ve outlined the system’s needs, let’s pinpoint the building blocks to fulfill them.

Identify Core Objects

Before diving into the design, it’s important to identify the core objects.

  • Locker: This class represents an individual locker.
  • Site: This class represents a locker facility that consists of multiple lockers of various sizes. It is responsible for managing the collection of lockers and organizing them by size.
  • ShippingPackage: An interface defining the standard for packages, with BasicShippingPackage as its concrete implementation, tracking details like order ID, dimensions, and status.
  • Account: This class represents customers and their associated accounts. Customers own packages stored in lockers, and their accounts store policy information for free and maximum storage periods, along with their current balance.

Design Class Diagram

Now that we know the core objects and their roles, the next step is to create classes and methods that turn the requirements into an easy-to-maintain system. Let’s take a closer look.

Locker

The Locker class represents a physical storage unit for holding packages. It includes the following attributes:

  • LockerSize size: Represents the size of the locker.
  • ShippingPackage currentPackage: Stores the package currently assigned to the locker.
  • Date assignmentDate: Tracks the date when the package was placed in the locker.
  • String accessCode: A unique security code required for package retrieval.

Below is the representation of this class. Image represents a UML class diagram for a `Locker` class.  The diagram is divided into two sections. The top section lists the class's attributes:  `LockerSize size` \(a `LockerSize` object\), `ShippingPackage currentPackage` \(a `ShippingPackage` object\), `Date assignmentDate` \(a `Date` object\), and `String accessCode` \(a string\).  These are all private attributes, indicated by the minus sign preceding each. The bottom section details the class's methods: a constructor `Locker\(LockerSize size\)`, a method `assignPackage\(ShippingPackage pkg, Date date\)` to assign a package to the locker, a method `releaseLocker\(\)` to release the locker, a method `calculateStorageCharges\(\)` returning a `BigDecimal` value representing storage charges, a method `isAvailable\(\)` returning a boolean indicating locker availability, and a method `checkAccessCode\(String code\)` returning a boolean based on access code verification. All methods are public, indicated by the plus sign.  The class name, `Locker`, is prominently displayed at the top, along with the standard UML class notation 'C'.  There are no connections or information flows depicted beyond the internal structure of the `Locker` class itself. It provides functionalities such as assigning a package to the locker, releasing the locker upon package retrieval, calculating storage charges based on usage duration, determining locker availability, and ensuring secure access through code verification. Design Choice Design choice: The Locker class is designed as a standalone entity to encapsulate the state and behavior of an individual locker, ensuring modularity and ease of maintenance.

LockerSize

The LockerSize enum represents the predefined sizes of lockers available in the system. Each size is associated with specific attributes that define its dimensions and daily usage charge.

  • String sizeName : A label identifying the locker size (e.g., 'Small,' 'Medium,' 'Large').
  • BigDecimal dailyCharge : The cost per day for using a locker of a specific size (“Small,” “Medium,” or “Large”).
  • BigDecimal width, height, depth : The physical dimensions of the locker, determining its capacity to accommodate different package sizes.

Image represents a UML class diagram depicting an entity named `LockerSize`.  The entity is denoted by the letter 'E' enclosed in a circle at the top left.  The main label `LockerSize` is positioned to the right of the 'E'. Below this, the entity's attributes are listed. These attributes include `sizeName` of type `String`, and `dailyCharge`, `width`, `height`, and `depth`, all of type `BigDecimal`.  Each attribute is prefixed with a minus sign and a square, indicating that they are private instance variables within the `LockerSize` entity.  No methods or relationships to other entities are shown in this diagram; it solely focuses on the internal structure and data types of the `LockerSize` entity. Design Choice Design choice: We use an enum here because locker sizes, such as Small, Medium, and Large, are a fixed set of options that don’t change during runtime, ensuring type safety and simplicity.

Site

The Site class models a physical location containing a collection of lockers, organized by their size. Key functionalities include:

  • Locker findAvailableLocker(LockerSize size): This method searches for an empty locker that matches the exact size you need, like "small," "medium," or "large."
  • Locker placePackage(ShippingPackage pkg, Date date): This method takes a package, finds it a locker that fits, and locks it inside. It also keeps track of when the package was placed there and updates the package’s status.

Image represents a class diagram showing a class named 'Site'.  The class 'Site' contains a private member variable named 'lockers' which is a map.  The keys of this map are of type 'LockerSize', and the values are sets of 'Locker' objects.  This indicates that the Site manages lockers categorized by their size, with multiple lockers potentially sharing the same size.  The class also has two public methods:  `findAvailableLocker\(LockerSize size\)`, which takes a LockerSize as input and presumably returns an available locker of that size; and `placePackage\(ShippingPackage pkg, Date date\)`, which takes a ShippingPackage and a Date as input and presumably places the package into a locker.  No connections to other classes are shown in this diagram, focusing solely on the internal structure and functionality of the 'Site' class. The lockers at each site are managed using a map-based structure, where the key is a LockerSize and the value is a set of lockers for that size. This structure allows quick access to available lockers based on their size.

ShippingPackage

We have modeled ShippingPackage as an interface to establish a standard for all package types within the locker system. It defines key methods that any package type must implement to ensure compatibility with locker storage and retrieval processes. The BasicShippingPackage class is a concrete implementation of the ShippingPackage interface. It represents a standard package intended for storage in a locker. The ShippingStatus enum defines a fixed set of valid states for a package’s lifecycle in the locker system, such as PENDING, STORED, and RETRIEVED. This enum ensures that package status updates are consistent and restricted to predefined values, enhancing type safety. The UML diagram below illustrates this structure. Image represents a UML class diagram illustrating the design of a `ShippingPackage`.  The diagram features an interface `ShippingPackage` defining eight methods: `getOrderId\(\)` \(returning a String\), `getUser\(\)` \(returning an Account object\), `getWidth\(\)`, `getHeight\(\)`, and `getDepth\(\)` \(all returning BigDecimal values representing package dimensions\), `getStatus\(\)` \(returning a `ShippingStatus` enum\), `updateShippingStatus\(\)` \(taking a `ShippingStatus` as a parameter and returning void\), and `getLockerSize\(\)` \(returning a `LockerSize` object, not further defined\).  A class `BasicShippingPackage` is shown implementing the `ShippingPackage` interface, indicated by a dashed line with a hollow triangle pointing towards `BasicShippingPackage`. Separately, an enumeration `ShippingStatus` is defined, listing possible shipping statuses: CREATED, PENDING, IN_LOCKER, RETRIEVED, and EXPIRED.  No data flows are explicitly shown; the diagram focuses on the structure and relationships between the interface, class, and enumeration. ShippingPackage interface and ShippingStatus enum Design Choice Design choice: Modeling ShippingPackage as an interface allows for extensibility, enabling the system to support diverse package types (e.g., fragile or perishable) without modifying core logic.

Account

The Account class represents a customer using the locker system. It maintains the customer’s balance for locker-related charges and is associated with an AccountLockerPolicy, which defines terms for free usage limits and maximum storage duration. This ensures that charges are applied in accordance with the policy. Image represents a class diagram for an `Account` class in an object-oriented system.  The diagram is a rectangular box with the class name 'Account' and a 'C' symbol in a circle in the upper right corner, indicating it's a class. Inside the box, four private attributes are listed: `accountId` \(String\), `ownerName` \(String\), `lockerPolicy` \(of type `AccountLockerPolicy`\), and `usageCharges` \(BigDecimal\).  These attributes represent data stored within each `Account` object. Below the attributes, a public method `addUsageCharge` is defined, taking a `BigDecimal` amount as a parameter and returning a boolean value, presumably indicating success or failure of adding the charge.  There are no connections or interactions with other classes depicted in this specific diagram; it solely focuses on the internal structure and behavior of the `Account` class itself. The class provides functionality to add funds to the account. Design Choice Design choice: By associating an AccountLockerPolicy, it supports flexible billing rules based on customer policies. This separation of customer and policy data enhances maintainability and allows for personalized locker usage terms.

AccountLockerPolicy

This class defines the rules and policies for locker usage associated with an account. It includes the number of days during which locker usage is free (freePeriodDays) and the maximum number of days a package can remain in the locker before it must be cleared (maximumPeriodDays). Here is the representation of this class. Image represents a class diagram depicting a class named `AccountLockerPolicy`.  The class is represented by a rectangular box with a large 'C' in a circle at the top-left corner, indicating it's a class in the Unified Modeling Language \(UML\). The class name, `AccountLockerPolicy`, is written to the right of the 'C'. Below the class name, the diagram shows two private instance variables: `int freePeriodDays` and `int maximumPeriodDays`.  Both variables are declared as integers \(`int`\) and prefixed with a minus sign \(`-`\), indicating their private access modifier, meaning they can only be accessed from within the `AccountLockerPolicy` class itself.  There are no methods or relationships shown in this diagram; it solely focuses on the class's internal data members.

NotificationInterface

Defines the contract for sending notifications. It includes a single method, sendNotification, which takes a message and an account as parameters. Image represents a UML diagram depicting a single interface named `NotificationInterface`.  The diagram is rectangular, divided into two sections by a horizontal line. The top section displays a large, grey circle containing the letter 'I' – indicating an interface – followed by the interface's name, `NotificationInterface`. The bottom section shows a single method declaration within the interface: `void sendNotification\(String message, Account user\)`. This method signature indicates that the `sendNotification` method takes a `String` representing the message and an `Account` object representing the user as input parameters, and it returns nothing \(`void`\).  There are no connections or arrows shown, indicating this is a simple definition of the interface and not a depiction of its interaction with other classes. Implementations of this interface manage user notifications, such as alerts for package pickup availability and locker usage fees. Best Practices Best practices: In OOD interviews, external systems like notifications are often represented as interfaces to keep the design flexible and scalable while avoiding unnecessary complexity.

LockerManager

The LockerManager class is responsible for managing package storage and retrieval at a specific site, ensuring that packages are assigned to suitable lockers based on size availability. It works with the following components:

  • Site site: Represents the physical location of the lockers.
  • NotificationInterface notificationService: Sends notifications to customers when their packages are assigned to a locker or ready for pick-up.
  • Map <String, Account> accounts: Maintains a mapping of account IDs to user accounts for managing locker usage and charges.
  • Map <String, Locker> accessCodeMap: Maintains a mapping of access codes to lockers, allowing for quick retrieval during package pick-up.

Image represents a low-resolution, grayscale icon depicting a black, five-pointed star centered within a larger, light-gray circular background.  The star is solid black, and its points extend nearly to the edge of the circle. The circle acts as a border or frame for the star, creating a visually distinct symbol. There is no text, URLs, or other parameters visible within the image; the image solely consists of the star and circular background, with no discernible connections or information flow to or from it. The overall impression is a simple, possibly symbolic representation of quality, achievement, or rating, given the common use of stars in such contexts. Design Choice Design choice: The LockerManager class is designed as a facade to simplify interactions between core objects, providing a single point of control for package assignment and retrieval.

Complete Class Diagram

Below is the complete class diagram of the shipping locker service: Image represents a class diagram illustrating the object-oriented design for a locker management system.  The diagram shows several classes and an interface, connected by various relationships.  `LockerManager` is the central class, containing a `Site` object, a `NotificationInterface` for sending notifications, and maps for managing accounts \(`accounts`\) and locker access codes \(`accessCodeMap`\).  `LockerManager` uses the `Site` to `assignPackage` and `pickUpPackage`. The `Site` class manages a map of `LockerSize` to sets of `Locker` objects, providing methods to find available lockers \(`findAvailableLocker`\) and place packages \(`placePackage`\).  `Locker` objects have attributes for size \(`LockerSize`\), current package \(`ShippingPackage`\), assignment date, and access code.  They offer methods for package assignment, release, storage charge calculation, availability check, and access code verification.  `ShippingPackage` is an interface implemented by `BasicShippingPackage`, which includes attributes for order ID, user \(`Account`\), dimensions, and status \(`ShippingStatus`\).  `ShippingPackage` provides methods to access these attributes and update the shipping status.  `Account` class stores account details, including a `AccountLockerPolicy` object which defines free and maximum usage periods, and manages usage charges.  `LockerSize` is an enumeration defining different locker sizes with associated daily charges and dimensions.  `ShippingStatus` is an enumeration representing the different states of a package.  Finally, `NotificationInterface` defines a method to send notifications to users.  The relationships between classes are shown through arrows indicating dependencies and aggregations. Class Diagram of Locker Service With the class diagram as our guide, let’s bring the system to life through code.

Code - Shipping Locker Service

After completing the class diagram, the interviewer might ask you to implement key components of the shipping locker system. In this section, we’ll implement essential classes that handle locker operations, package assignments, and notifications.

Locker and LockerSize

The Locker class represents an individual locker unit. Each locker has a fixed size (an instance of LockerSize) and maintains details about the currently stored package, including the assignment date and a randomly generated access code for secure retrieval. This class provides functionalities for:

  • Assigning a package to a locker, recording the assignment date, and generating a secure access code.
  • Calculating storage charges based on the number of days the package has been stored and the locker’s daily rate, according to its size.
  • Once the package is picked up, make the locker available for future use.

Below is the implementation of this class.

public class Locker {
    // Size of the locker
    private final LockerSize size;
    // Currently stored package
    private ShippingPackage currentPackage;
    // Date when the current package was assigned
    private Date assignmentDate;
    // Access code for retrieving the package
    private String accessCode;

    public Locker(LockerSize size) {
        this.size = size;
    }

    // Assigns a package to this locker and generates an access code
    public void assignPackage(ShippingPackage pkg, Date date) {
        this.currentPackage = pkg;
        this.assignmentDate = date;
        this.accessCode = generateAccessCode();
    }

    // Releases the locker by removing the current package and its details
    public void releaseLocker() {
        this.currentPackage = null;
        this.assignmentDate = null;
        this.accessCode = null;
    }

    // Calculates storage charges based on usage duration and policy
    public BigDecimal calculateStorageCharges() {
        if (currentPackage == null || assignmentDate == null) {
            return BigDecimal.ZERO;
        }

        AccountLockerPolicy policy = currentPackage.getUser().getLockerPolicy();
        long totalDaysUsed =
                (new Date().getTime() - assignmentDate.getTime()) / (1000 * 60 * 60 * 24);

        // Check if exceeds maximum period
        if (totalDaysUsed > policy.getMaximumPeriodDays()) {
            currentPackage.updateShippingStatus(ShippingStatus.EXPIRED);
            throw new MaximumStoragePeriodExceededException(
                    "Package has exceeded maximum allowed storage period of "
                            + policy.getMaximumPeriodDays()
                            + " days");
        }

        // Calculate chargeable days (excluding free period)
        long chargeableDays = Math.max(0, totalDaysUsed - policy.getFreePeriodDays());
        return size.dailyCharge.multiply(new BigDecimal(chargeableDays));
    }

    // Checks if the locker is available for new packages
    public boolean isAvailable() {
        return currentPackage == null;
    }

    // Verifies if the provided access code matches the locker's code
    public boolean checkAccessCode(String code) {
        return this.accessCode != null && accessCode.equals(code);
    }

    // getter and setter methods are omitted for brevity
}

The LockerSize enum defines different locker sizes, each with an associated daily charge rate and physical dimensions (width, height, and depth). These predefined sizes allow the system to categorize lockers and ensure packages are placed in appropriately sized compartments.

public enum LockerSize {
    // Small locker with 10x10x10 dimensions and $5 daily charge
    SMALL(
            "Small",
            new BigDecimal("5.00"),
            new BigDecimal("10.00"),
            new BigDecimal("10.00"),
            new BigDecimal("10.00")),
    // Medium locker with 20x20x20 dimensions and $10 daily charge
    MEDIUM(
            "Medium",
            new BigDecimal("10.00"),
            new BigDecimal("20.00"),
            new BigDecimal("20.00"),
            new BigDecimal("20.00")),
    // Large locker with 30x30x30 dimensions and $15 daily charge
    LARGE(
            "Large",
            new BigDecimal("15.00"),
            new BigDecimal("30.00"),
            new BigDecimal("30.00"),
            new BigDecimal("30.00"));

    // Name of the locker size
    final String sizeName;
    // Daily charge for using this size locker
    final BigDecimal dailyCharge;
    // Width of the locker in inches
    final BigDecimal width;
    // Height of the locker in inches
    final BigDecimal height;
    // Depth of the locker in inches
    final BigDecimal depth;

    // Creates a new locker size with specified dimensions and charges
    LockerSize(
            String sizeName,
            BigDecimal dailyCharge,
            BigDecimal width,
            BigDecimal height,
            BigDecimal depth) {
        this.sizeName = sizeName;
        this.dailyCharge = dailyCharge;
        this.width = width;
        this.height = height;
        this.depth = depth;
    }
    // getter methods are omitted for brevity
}

Implementation choice: You might have noticed that we have used BigDecimal for dailyCharge and dimensions in the LockerSize enum to ensure precision in financial calculations, unlike float or double, which could introduce rounding errors.

Site

The Site class represents a physical location containing multiple lockers, organized by size for efficient storage management. It maintains a collection of lockers grouped by size, treating lockers of the same size as interchangeable. This means any available locker of a given size can store a package, simplifying locker assignment. To efficiently manage lockers, the class uses a map-based structure, where:

  • Each key is a LockerSize, representing a specific locker category ("Small," "Medium," "Large").
  • Each value is a Set, containing all lockers of that size.

This design allows the system to quickly find an available locker and assign packages accordingly. Here is the implementation of this class.

public class Site {
    // Map of locker sizes to sets of lockers of that size
    final Map<LockerSize, Set<Locker>> lockers = new HashMap<>();

    // Creates a new site with specified number of lockers for each size
    public Site(Map<LockerSize, Integer> lockers) {
        for (Map.Entry<LockerSize, Integer> entry : lockers.entrySet()) {
            Set<Locker> lockerSet = new HashSet<>();
            for (int i = 0; i < entry.getValue(); i++) {
                lockerSet.add(new Locker(entry.getKey()));
            }
            this.lockers.put(entry.getKey(), lockerSet);
        }
    }

    // Finds an available locker of the specified size
    public Locker findAvailableLocker(LockerSize size) {
        for (Locker locker : lockers.get(size)) {
            if (locker.isAvailable()) {
                return locker;
            }
        }
        return null;
    }

    // Places a package in an available locker of appropriate size
    public Locker placePackage(ShippingPackage pkg, Date date) {
        // Determine the smallest locker size that can fit this package
        LockerSize size = pkg.getLockerSize();
        Locker locker = findAvailableLocker(size);
        if (locker != null) {
            locker.assignPackage(pkg, date);
            pkg.updateShippingStatus(ShippingStatus.IN_LOCKER);
            return locker;
        }
        throw new NoLockerAvailableException(
                "No locker of size " + size + " is currently available");
    }
}

  • findAvailableLocker(LockerSize size): Locates and returns an available locker of the specified size.
  • placePackage(ShippingPackage pkg, Date date): A convenience method that finds an available locker and assigns the package in one step. If no locker is available, it returns null.

Implementation choice: While the same result could be achieved by separately calling findAvailableLocker() and then manually assigning a package, providing helper methods such as placePackage() is beneficial. It provides atomic transaction-like behavior by encapsulating the find-and-assign operations in a single method call, ensuring thread safety and preventing inconsistent states that could occur if these operations were performed separately.

BasicShippingPackage

The BasicShippingPackage class represents a standard package within the locker system, implementing the ShippingPackage interface. It encapsulates essential package details such as order ID, user account, dimensions (width, height, depth), and shipping status. These attributes allow the system to determine where the package can be stored. Here is the implementation of this class.

public class BasicShippingPackage implements ShippingPackage {
    // Unique identifier for the order
    private final String orderId;
    // User account associated with this package
    private final Account user;
    private final BigDecimal width;
    private final BigDecimal height;
    private final BigDecimal depth;
    // Current status of the package
    private ShippingStatus status;

    // Creates a new shipping package with specified dimensions
    public BasicShippingPackage(
            String orderId, Account user, BigDecimal width, BigDecimal height, BigDecimal depth) {
        this.orderId = orderId;
        this.user = user;
        this.width = width;
        this.height = height;
        this.depth = depth;
        this.status = ShippingStatus.CREATED;
    }

    // Returns the current package status
    @Override
    public ShippingStatus getStatus() {
        return status;
    }

    // Updates the package status
    @Override
    public void updateShippingStatus(ShippingStatus status) {
        this.status = status;
    }

    // Determines the smallest locker size that can fit this package
    @Override
    public LockerSize getLockerSize() {
        for (LockerSize size : LockerSize.values()) {
            if (size.getWidth().compareTo(width) >= 0
                    && size.getHeight().compareTo(height) >= 0
                    && size.getDepth().compareTo(depth) >= 0) {
                return size;
            }
        }
        throw new PackageIncompatibleException("No locker size available for the package");
    } // getter methods are omitted for brevity
}

A key functionality of this class is determining the appropriate locker size for a package. The getLockerSize() method evaluates all available locker sizes ("Small," "Medium," "Large") and returns the smallest suitable locker based on the package’s dimensions. Additionally, the updateShippingStatus() method allows the locker system to communicate status updates (e.g., when a package is stored or retrieved) to the user.

Account and AccountLockerPolicy

The Account class represents a user’s account within the locker system. It stores basic account details, including the accountId, ownerName, the current usage charges, and the user’s lockerPolicy (an instance of AccountLockerPolicy), which defines the user-specific rules for locker usage. The account usageCharges is used to track locker usage fees, and funds can be added dynamically using the addUsageCharge(BigDecimal amount) method.

public class Account {
    private final String accountId;
    private final String ownerName;
    // Policy defining locker usage rules for this account
    private final AccountLockerPolicy lockerPolicy;
    // Total charges accumulated for locker usage
    private BigDecimal usageCharges = new BigDecimal("0.00");

    // Creates a new account with specified details and policy
    public Account(String accountId, String ownerName, AccountLockerPolicy lockerPolicy) {
        this.accountId = accountId;
        this.ownerName = ownerName;
        this.lockerPolicy = lockerPolicy;
    }

    // Adds a charge to the account's total usage charges
    public void addUsageCharge(BigDecimal amount) {
        usageCharges = usageCharges.add(amount);
    }

    // getter methods are omitted for brevity
}

The AccountLockerPolicy class encapsulates the rules for locker usage. It specifies:

  • freePeriodDays: The number of days a user can store a package without incurring charges.
  • maximumPeriodDays: The maximum number of days a package can remain in a locker before further action is required (e.g., additional charges or forced retrieval).

This policy allows customized locker usage limits, ensuring that different users can have varying storage privileges based on their account settings. Check out the implementation of this class.

public class AccountLockerPolicy {
    // Number of days of free storage
    final int freePeriodDays;
    // Maximum number of days a package can be stored
    final int maximumPeriodDays;

    // Creates a new locker policy with specified free and maximum periods
    public AccountLockerPolicy(int freePeriodDays, int maximumPeriodDays) {
        this.freePeriodDays = freePeriodDays;
        this.maximumPeriodDays = maximumPeriodDays;
    }
    // getter methods are omitted for brevity
}

LockerManager

The LockerManager class acts as a facade for the overall locker system. It serves as the central coordinator for the locker system, coordinating package assignments to lockers, package pick-ups, and locker releases. It interacts with Site, which oversees the locker inventory and handles the actual placement of packages in available lockers. The NotificationInterface is responsible for sending user notifications regarding package assignments and pickups. Additionally, it maintains a Map<String, Account> to store customer accounts, which are used to track locker usage charges. To ensure secure and efficient package retrieval, it uses a Map<String, Locker> to map access codes to their corresponding lockers. Below is the implementation of this class.

public class LockerManager {
    // The site being managed
    private final Site site;
    // Service for sending notifications
    private final NotificationInterface notificationService;
    // Map of account IDs to account objects
    private final Map<String, Account> accounts;
    // Map of access codes to lockers
    private final Map<String, Locker> accessCodeMap = new HashMap<>();

    // Creates a new locker manager for a site
    public LockerManager(
            Site site, Map<String, Account> accounts, NotificationInterface notificationService) {
        this.site = site;
        this.accounts = accounts;
        this.notificationService = notificationService;
    }

    // Assigns a package to an available locker
    public Locker assignPackage(ShippingPackage pkg, Date date) {
        Locker locker = site.placePackage(pkg, date);
        if (locker != null) {
            accessCodeMap.put(locker.getAccessCode(), locker);
            notificationService.sendNotification(
                    "Package assigned to locker" + locker.getAccessCode(), pkg.getUser());
        }
        return locker;
    }

    // Processes package pickup using an access code
    public Locker pickUpPackage(String accessCode) {
        Locker locker = accessCodeMap.get(accessCode);
        if (locker != null && locker.checkAccessCode(accessCode)) {
            try {
                BigDecimal charge = locker.calculateStorageCharges();
                ShippingPackage pkg = locker.getPackage();
                locker.releaseLocker();
                pkg.getUser().addUsageCharge(charge);
                pkg.updateShippingStatus(ShippingStatus.RETRIEVED);
                return locker;
            } catch (MaximumStoragePeriodExceededException e) {
                locker.releaseLocker();
                return locker;
            }
        }
        return null;
    }
    // getter methods are omitted for brevity
}

  • assignPackage(ShippingPackage pkg, Date date): Assigns a package to an available locker via site.placePackage().
  • pickUpPackage(String accessCode): Retrieves the package if the provided access code matches the stored locker. It then:
    • Calculates charges based on locker usage duration.
    • Updates the user’s usage charges.
    • Releases the locker and marks the package as retrieved.

Best Practices Best practices: In complex systems, a manager class like LockerManager is commonly used to centralize operations such as package assignment, retrieval, and notifications. However, to maintain a thin manager class, it is essential to delegate low-level operations, such as finding available lockers, storing packages, and enforcing locker policies, to specialized classes like Site and Locker.

Deep Dive Topic

Now that the basic design is complete, the interviewer might ask you some deep dive questions. Let’s check out some of these.

Designing an extensible locker creation system

Currently, lockers are created directly based on predefined sizes, which makes the system rigid and difficult to extend. For example, if we need to introduce a new locker size (e.g., XLARGE) or a specialized locker type (e.g., temperature-controlled lockers), we would have to modify multiple parts of the codebase where lockers are instantiated manually. This increases maintenance overhead and reduces flexibility. To address this, we can use the Factory design pattern to centralize the creation logic for different types of lockers, making the system more modular and extensible. Note : To learn more about the Factory Pattern and its common use cases, refer to the Further Reading section at the end of this chapter. Here is the code for the LockerFactory class.

class LockerFactory {
    // Creates a new locker of the specified size
    public static Locker createLocker(LockerSize size) {
        return switch (size) {
            case SMALL -> new Locker(LockerSize.SMALL);
            case MEDIUM -> new Locker(LockerSize.MEDIUM);
            case LARGE -> new Locker(LockerSize.LARGE);
            case XLARGE -> new Locker(LockerSize.XLARGE);
        };
    }
}

Here’s how a LockerFactory class enhances the locker system:

  • Centralized object creation: The LockerFactory encapsulates the locker instantiation process, so any changes to locker creation (e.g., adding new sizes or types) are localized to this class.
  • Extensibility: If we need to add new locker sizes or types in the future, the factory class can be easily updated, without modifying core business logic in other parts of the system.
  • Improved readability and maintainability: Using a factory method to create lockers allows us to separate locker creation from locker usage logic, aligning with the Single Responsibility Principle.

Decoupling event handling with an event-driven approach

Currently, the LockerManager directly invokes NotificationInterface.sendNotification() to notify customers when key events occur, such as package assignment or locker expiration. While this approach works, it tightly couples LockerManager with the notification system, meaning any changes to how notifications are sent require modifying LockerManager itself. How can we improve the system design to make event handling more flexible and scalable? To decouple event handling from LockerManager, we can use an event-driven approach where multiple components can subscribe to and respond to system events dynamically. Instead of directly calling sendNotification(), LockerManager will broadcast events to registered observers, ensuring a more modular and extensible system. By implementing the Observer pattern, we can send notifications to customers or administrative staff when certain events occur, such as when a package is delivered or a locker exceeds its maximum usage period. Note : To learn more about the Observer Pattern and its common use cases, refer to the Elevator System chapter. Instead of hardcoding the notification logic in the LockerManager, the locker system can maintain a list of observers. These observers can be various notification services (e.g., email, SMS) or other systems (e.g., analytics, metrics). When a key event occurs, the LockerManager can notify all registered observers, allowing the system to be easily extended without modifying the core logic. Below is the implementation of the relevant interface and classes for the observer pattern.

  • Subject : LockerManagerChange, which maintains a list of observers and notifies them of state changes, such as package assignments.
class LockerManagerChange {
    // List of observers that will be notified of locker events
    private final List<LockerEventObserver> observers = new ArrayList<>();

    public void addObserver(LockerEventObserver observer) {
        observers.add(observer);
    }

    public void removeObserver(LockerEventObserver observer) {
        observers.remove(observer);
    }

    private void notifyObservers(String message, Account account) {
        for (LockerEventObserver observer : observers) {
            observer.update(message, account);
        }
    }

    // Assigns a package to an available locker and notifies observers.
    public void assignPackage(ShippingPackage pkg) {
        Locker locker = assignLockerToPackage(pkg);
        if (locker != null) {
            notifyObservers("Package assigned to locker", pkg.getUser());
        }
    }

    private Locker assignLockerToPackage(ShippingPackage pkg) {
        return null;
    }
}

  • Observer : The LockerEventObserver interface, which defines the update() method that observers must implement to handle notifications.
// Interface for objects that need to be notified of locker events
public interface LockerEventObserver {
    // Updates the observer with a message and the affected account
    void update(String message, Account account);
}
  • Concrete Observers : Classes like EmailNotification, which implement LockerEventObserver to send email alerts for specific events, enabling flexible and extensible event handling.
// Implementation of LockerEventObserver that sends email notifications
class EmailNotification implements LockerEventObserver {
    // Updates the observer with a message and sends it to the account owner
    public void update(String message, Account account) {
        // send email to account owner
    }
}

Wrap Up

In this chapter, we designed a Shipping Locker System. We began by clarifying requirements through a structured Q&A discussion, similar to an interviewer-candidate exchange. From there, we identified core objects and developed a class diagram to represent the system's structure. We implemented the key components of the locker service, bringing the design to life. In the deep dive section, we explored advanced topics, including how the Factory Pattern simplifies locker instantiation for future scalability and how the Observer Pattern decouples event handling, enabling flexible notifications. Congratulations on getting this far! Now give yourself a pat on the back. Good job!

Further Reading: Factory Design Pattern

This section gives a quick overview of the design patterns used in this chapter. It’s helpful if you’re new to these patterns or need a refresher to better understand the design choices.

Factory design pattern

The Factory Method Pattern is a creational design pattern that provides an interface for creating instances of a class but allows subclasses to alter the type of objects that will be created. In the shipping locker system, we use the Factory pattern through the LockerFactory class to centralize locker creation, enabling easy addition of new locker sizes (e.g., XLARGE) without modifying existing code. To illustrate the Factory pattern in another domain, the following example uses an e-commerce payment system. Problem Imagine building an e-commerce payment system that initially supports only credit card payments, with logic tied to a CreditCardPayment class. As demand grows for new payment options like digital wallets or cryptocurrency, adding them requires altering multiple parts of the tightly coupled codebase, increasing maintenance effort, and reducing flexibility. Solution Image represents a class diagram illustrating a factory pattern implementation for payment processing.  At the top is a class named `PaymentFactory` containing a single public method `createPayment\(type: String\): Payment`. This method takes a string as input, presumably representing the payment type, and returns an object of type `Payment`. An arrow labeled 'Creates' points downwards from `PaymentFactory` to an interface named `Payment`.  The `Payment` interface defines a single method `processPayment\(amount: double\)`, which takes a double representing the payment amount as input.  Two classes, `CreditCardPayment` and `PayPalPayment`, implement the `Payment` interface, each providing its own implementation of the `processPayment` method.  Dashed arrows indicate that `CreditCardPayment` and `PayPalPayment` are implementations of the `Payment` interface.  The overall structure shows how `PaymentFactory` creates concrete payment objects based on the input type, which then utilize the `processPayment` method defined in the `Payment` interface. Factory design pattern The Factory Method pattern addresses this by replacing direct object creation (using the new operator) with a factory method that encapsulates instantiation. This allows new payment types to be added via subclasses without changing existing code, decoupling payment processing from the main system for better scalability and maintainability. When to use The factory design pattern is particularly useful in the following scenarios:

  • Use the Factory Method when the specific types and dependencies of objects required by your code are unknown in advance.
  • Use the Factory Method to allow users of your library or framework to extend its internal components without modifying existing code.
  • Use the Factory Method to optimize resource utilization by reusing existing objects rather than creating new instances each time.

Mark as Complete ask alexask alex expend In this chapter, we will design a Shipping Locker system similar to UPS, FedEx, or Amazon Locker. It offers customers a convenient and secure way to pick up their online orders. The system manages locker availability, assigns incoming packages to appropriate lockers, and ensures a smooth package retrieval process for customers. To realize this vision of a convenient and secure locker system, let’s explore what it needs to do. Image represents a simplified illustration of a self-service locker system.  A cartoon girl stands to the left, interacting with a bank of lockers depicted as a gray rectangular unit with a shelf-like top and bottom. The locker unit contains twelve individual compartments arranged in a four-by-three grid. One compartment is partially open, revealing a darker gray interior, suggesting an item is being retrieved. The girl is shown holding a similar-sized square object, implying she is either placing an item in or retrieving one from a locker. On the rightmost side of the locker unit is a small control panel labeled 'PICKUP,' featuring a gray display screen and three buttons below it, suggesting a user interface for selecting and accessing lockers. The overall style is simplistic and cartoonish, focusing on the interaction between the user and the locker system. Shipping Locker System