Facebook Pixel

3822. Design Order Management System 🔒

MediumDesignHash Table
LeetCode ↗

Problem Description

You are asked to design a simple order management system for a trading platform.

Each order has three pieces of information:

  • An orderId — a unique identifier for the order.
  • An orderType — either "buy" or "sell".
  • A price — the price associated with the order.

An order is considered active as long as it has not been canceled.

You need to implement the OrderManagementSystem class with the following methods:

  • OrderManagementSystem(): Initializes the order management system.
  • void addOrder(int orderId, string orderType, int price): Adds a new active order with the given orderId, orderType, and price. It is guaranteed that the orderId is unique.
  • void modifyOrder(int orderId, int newPrice): Changes the price of an existing order to newPrice. It is guaranteed that the order exists and is currently active.
  • void cancelOrder(int orderId): Cancels an existing order, making it no longer active. It is guaranteed that the order exists and is currently active.
  • vector<int> getOrdersAtPrice(string orderType, int price): Returns the orderIds of all active orders that match both the given orderType and price. If no such orders exist, return an empty list.

Note: The order of the returned orderIds does not matter.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Design + Supporting Data Structures?

This problem maps to Design + Supporting Data Structures through a short path in the full flowchart.

Implementaclass/system?yesComplexdatastructuresyesDesign +Supporting DataStructures

Designing an order management system with add/modify/cancel/get-operations at-price requires hash tables and ordered mappings for efficient lookup.

Open in Flowchart

Intuition

The key challenge in this problem is supporting two different kinds of lookups efficiently:

  1. Given an orderId, we need to quickly find its current orderType and price. This is needed by modifyOrder and cancelOrder, since they only receive the orderId but must locate the order's existing attributes to update or remove it.

  2. Given an (orderType, price) pair, we need to quickly find all matching active orders. This is exactly what getOrdersAtPrice requires.

If we only stored orders in a single structure, at least one of these lookups would require scanning through everything, which would be slow. The trick is to maintain two hash tables at the same time, each optimized for one direction of lookup.

The first hash table, orders, maps each orderId to its (orderType, price). This lets us instantly retrieve an order's attributes when we only know its ID.

The second hash table, t, maps each (orderType, price) pair to a list of orderIds that currently sit at that type and price. This lets getOrdersAtPrice return its answer directly, without any searching.

With both tables in place, every operation becomes simple:

  • For addOrder, we record the order in orders and append its ID to the list in t under the key (orderType, price).
  • For modifyOrder, we use orders to look up the old (orderType, price), remove the ID from that old bucket in t, then add it to the new bucket for (orderType, newPrice), and finally update orders.
  • For cancelOrder, we again look up the order's attributes from orders, remove its ID from the matching bucket in t, and delete it from orders.
  • For getOrdersAtPrice, we simply return the list stored under (orderType, price) in t.

The core idea is that by indexing the same data in two complementary ways, we trade a little extra storage for fast performance on every operation.

Solution Approach

We use a Hash Table based design with two complementary tables.

We maintain a hash table orders to store the type and price information of each order, where the key is the orderId and the value is a tuple (orderType, price). Additionally, we use another hash table t to store the list of order IDs corresponding to each (orderType, price), where the key is a tuple (orderType, price) and the value is the list of order IDs.

Here is how each operation is implemented:

  • addOrder(orderId, orderType, price): We add the order information to orders by setting orders[orderId] = (orderType, price), and append the orderId to the corresponding list in t under the key (orderType, price).

  • modifyOrder(orderId, newPrice): We first retrieve the order type and old price from orders. Then we update the order's price information by setting orders[orderId] = (orderType, newPrice). Next, we remove the orderId from the list in t keyed by the old (orderType, price), and add it to the list keyed by the new (orderType, newPrice).

  • cancelOrder(orderId): We retrieve the order type and price information from orders, delete the order from orders, and then remove the orderId from the corresponding list in t under the key (orderType, price).

  • getOrdersAtPrice(orderType, price): We directly return the list of order IDs corresponding to the key (orderType, price) in t. Since t is a defaultdict(list), a query for a non-existent key automatically returns an empty list.

In terms of complexity, addOrder runs in O(1) time. Both modifyOrder and cancelOrder take O(n) time in the worst case, because the remove operation on a list may need to scan through all order IDs sharing the same (orderType, price), where n is the number of such orders. The getOrdersAtPrice operation runs in O(1) time since it returns the list reference directly. The overall space complexity is O(m), where m is the total number of active orders.

Example Walkthrough

Let's trace through a small sequence of operations to see how the two hash tables stay in sync. We'll keep track of both orders (mapping orderId → (orderType, price)) and t (mapping (orderType, price) → [orderIds]) at each step.

Initial state

orders = {}
t      = {}

Step 1: addOrder(1, "buy", 100)

We record the order in orders, then append its ID to the bucket in t for ("buy", 100).

orders = { 1: ("buy", 100) }
t      = { ("buy", 100): [1] }

Step 2: addOrder(2, "buy", 100)

A second buy order at the same price. It lands in the same bucket of t.

orders = { 1: ("buy", 100), 2: ("buy", 100) }
t      = { ("buy", 100): [1, 2] }

Step 3: addOrder(3, "sell", 100)

Same price, but different type, so this gets its own bucket.

orders = { 1: ("buy", 100), 2: ("buy", 100), 3: ("sell", 100) }
t      = { ("buy", 100): [1, 2], ("sell", 100): [3] }

Step 4: getOrdersAtPrice("buy", 100)

We look up the key ("buy", 100) in t and return its list directly — no searching needed.

Returns [1, 2]

Step 5: modifyOrder(1, 200)

We only know orderId = 1, so we use orders to discover its current attributes: ("buy", 100).

  • Remove 1 from the old bucket t[("buy", 100)].
  • Add 1 to the new bucket t[("buy", 200)].
  • Update orders[1] to ("buy", 200).
orders = { 1: ("buy", 200), 2: ("buy", 100), 3: ("sell", 100) }
t      = { ("buy", 100): [2], ("sell", 100): [3], ("buy", 200): [1] }

Step 6: cancelOrder(2)

Using orders, we find that order 2 is ("buy", 100).

  • Remove 2 from the bucket t[("buy", 100)] (it becomes empty).
  • Delete 2 from orders.
orders = { 1: ("buy", 200), 3: ("sell", 100) }
t      = { ("buy", 100): [], ("sell", 100): [3], ("buy", 200): [1] }

Step 7: getOrdersAtPrice("buy", 100)

The bucket ("buy", 100) is now empty, so we get back an empty list (a defaultdict(list) returns [] for missing or empty keys).

Returns []

Why it works

Notice how every operation that starts with just an orderId (modifyOrder, cancelOrder) first consults orders to recover the (orderType, price), which acts as the key into t. Meanwhile, getOrdersAtPrice reads straight from t. By keeping both tables consistent on every write, each lookup direction stays fast — we never have to scan all orders to answer a query.

Solution Implementation

1from collections import defaultdict
2from typing import List
3
4
5class OrderManagementSystem:
6
7    def __init__(self) -> None:
8        # Maps orderId -> (orderType, price) for O(1) lookup of any order's details
9        self.order_details: dict[int, tuple[str, int]] = {}
10        # Maps (orderType, price) -> list of orderIds, grouping orders by their key
11        self.orders_by_key: defaultdict[tuple[str, int], List[int]] = defaultdict(list)
12
13    def addOrder(self, orderId: int, orderType: str, price: int) -> None:
14        # Record the order's type and price
15        self.order_details[orderId] = (orderType, price)
16        # Group this order under its (type, price) bucket
17        self.orders_by_key[(orderType, price)].append(orderId)
18
19    def modifyOrder(self, orderId: int, newPrice: int) -> None:
20        # Retrieve the existing type and old price
21        orderType, oldPrice = self.order_details[orderId]
22        # Update the stored details with the new price
23        self.order_details[orderId] = (orderType, newPrice)
24        # Move the order from its old bucket to the new one
25        self.orders_by_key[(orderType, oldPrice)].remove(orderId)
26        self.orders_by_key[(orderType, newPrice)].append(orderId)
27
28    def cancelOrder(self, orderId: int) -> None:
29        # Retrieve the order's type and price before deletion
30        orderType, price = self.order_details[orderId]
31        # Remove the order from the details map
32        del self.order_details[orderId]
33        # Remove the order from its (type, price) bucket
34        self.orders_by_key[(orderType, price)].remove(orderId)
35
36    def getOrdersAtPrice(self, orderType: str, price: int) -> List[int]:
37        # Return all orderIds matching the given type and price
38        return self.orders_by_key[(orderType, price)]
39
40
41# Your OrderManagementSystem object will be instantiated and called as such:
42# obj = OrderManagementSystem()
43# obj.addOrder(orderId,orderType,price)
44# obj.modifyOrder(orderId,newPrice)
45# obj.cancelOrder(orderId)
46# param_4 = obj.getOrdersAtPrice(orderType,price)
47```
48
49### Key changes made
50
511. **Standardized naming**: Renamed the cryptic `orders` and `t` attributes to descriptive names `order_details` and `orders_by_key`. Inside `modifyOrder`, the variable `price` was renamed to `oldPrice` for clarity, since it represents the price being replaced.
52
532. **Type annotations**: Added Python3-style type hints using built-in generics (`dict[...]`, `tuple[...]`, `defaultdict[...]`), which are valid in Python 3.9+. The required imports (`defaultdict`, `List`) are included explicitly.
54
553. **Comments**: Added comments explaining the purpose of each data structure and the intent behind each operation.
56
57### A note on a potential improvement
58
59The current `getOrdersAtPrice` returns a direct reference to the internal list. If a caller mutates the returned list, it would corrupt the internal state. A safer alternative is to return a copy:
60
61```python3
62def getOrdersAtPrice(self, orderType: str, price: int) -> List[int]:
63    # Return a copy so callers cannot mutate internal state
64    return list(self.orders_by_key[(orderType, price)])
65
1class OrderManagementSystem {
2
3    /**
4     * Composite key combining an order's type and price.
5     * Used to group orders that share the same (orderType, price) pair.
6     */
7    private record OrderKey(String orderType, int price) {
8    }
9
10    // Maps an order id to its order type.
11    private final Map<Integer, String> orderTypeById;
12
13    // Maps an order id to its current price.
14    private final Map<Integer, Integer> priceById;
15
16    // Maps an (orderType, price) key to the list of order ids matching it.
17    private final Map<OrderKey, List<Integer>> ordersByKey;
18
19    public OrderManagementSystem() {
20        orderTypeById = new HashMap<>();
21        priceById = new HashMap<>();
22        ordersByKey = new HashMap<>();
23    }
24
25    /**
26     * Adds a new order with the given id, type, and price.
27     */
28    public void addOrder(int orderId, String orderType, int price) {
29        // Record the order's type and price for quick lookup.
30        orderTypeById.put(orderId, orderType);
31        priceById.put(orderId, price);
32
33        // Index the order under its (type, price) key.
34        OrderKey key = new OrderKey(orderType, price);
35        ordersByKey.computeIfAbsent(key, k -> new ArrayList<>()).add(orderId);
36    }
37
38    /**
39     * Updates the price of an existing order, re-indexing it accordingly.
40     */
41    public void modifyOrder(int orderId, int newPrice) {
42        String orderType = orderTypeById.get(orderId);
43        int oldPrice = priceById.get(orderId);
44
45        // Update the stored price.
46        priceById.put(orderId, newPrice);
47
48        // Remove the order from its old (type, price) bucket.
49        // Cast to Integer so remove(Object) is invoked rather than remove(int index).
50        ordersByKey.get(new OrderKey(orderType, oldPrice)).remove((Integer) orderId);
51
52        // Add the order to its new (type, price) bucket.
53        ordersByKey.computeIfAbsent(new OrderKey(orderType, newPrice), k -> new ArrayList<>())
54                .add(orderId);
55    }
56
57    /**
58     * Cancels an order, removing it from all internal structures.
59     */
60    public void cancelOrder(int orderId) {
61        // Remove and capture the order's type and price.
62        String orderType = orderTypeById.remove(orderId);
63        int price = priceById.remove(orderId);
64
65        // Remove the order from its (type, price) bucket.
66        ordersByKey.get(new OrderKey(orderType, price)).remove((Integer) orderId);
67    }
68
69    /**
70     * Returns all order ids that have the given type and price.
71     */
72    public int[] getOrdersAtPrice(String orderType, int price) {
73        List<Integer> orders = ordersByKey.getOrDefault(new OrderKey(orderType, price), List.of());
74        return orders.stream().mapToInt(Integer::intValue).toArray();
75    }
76}
77
78/**
79 * Your OrderManagementSystem object will be instantiated and called as such:
80 * OrderManagementSystem obj = new OrderManagementSystem();
81 * obj.addOrder(orderId,orderType,price);
82 * obj.modifyOrder(orderId,newPrice);
83 * obj.cancelOrder(orderId);
84 * int[] param_4 = obj.getOrdersAtPrice(orderType,price);
85 */
86
1class OrderManagementSystem {
2    // Composite key: (orderType, price) used to group orders sharing the same type and price.
3    using Key = pair<string, int>;
4
5    // Custom hash functor so that Key can be used inside an unordered_map.
6    struct KeyHash {
7        size_t operator()(const Key& key) const {
8            // Combine the hash of the string part and the (shifted) hash of the int part.
9            return hash<string>()(key.first) ^ (hash<int>()(key.second) << 1);
10        }
11    };
12
13    // orderId -> orderType
14    unordered_map<int, string> orderTypeMap;
15    // orderId -> price
16    unordered_map<int, int> priceMap;
17    // (orderType, price) -> list of orderIds at that type/price bucket
18    unordered_map<Key, vector<int>, KeyHash> ordersByKey;
19
20public:
21    OrderManagementSystem() {}
22
23    // Insert a new order and register it in all three lookup structures.
24    void addOrder(int orderId, string orderType, int price) {
25        orderTypeMap[orderId] = orderType;
26        priceMap[orderId] = price;
27        ordersByKey[{orderType, price}].push_back(orderId);
28    }
29
30    // Change the price of an existing order, moving it to the correct bucket.
31    void modifyOrder(int orderId, int newPrice) {
32        string orderType = orderTypeMap[orderId];
33        int oldPrice = priceMap[orderId];
34
35        // Update the stored price.
36        priceMap[orderId] = newPrice;
37
38        // Remove the order from its old (type, oldPrice) bucket.
39        auto& oldList = ordersByKey[{orderType, oldPrice}];
40        oldList.erase(find(oldList.begin(), oldList.end(), orderId));
41
42        // Add the order to its new (type, newPrice) bucket.
43        ordersByKey[{orderType, newPrice}].push_back(orderId);
44    }
45
46    // Remove an order entirely from all lookup structures.
47    void cancelOrder(int orderId) {
48        string orderType = orderTypeMap[orderId];
49        int price = priceMap[orderId];
50
51        // Erase the metadata entries.
52        orderTypeMap.erase(orderId);
53        priceMap.erase(orderId);
54
55        // Erase the order from its (type, price) bucket.
56        auto& list = ordersByKey[{orderType, price}];
57        list.erase(find(list.begin(), list.end(), orderId));
58    }
59
60    // Return all order ids stored at the given (orderType, price) bucket.
61    vector<int> getOrdersAtPrice(string orderType, int price) {
62        auto it = ordersByKey.find({orderType, price});
63        if (it == ordersByKey.end()) return {};
64        return it->second;
65    }
66};
67
68/**
69 * Your OrderManagementSystem object will be instantiated and called as such:
70 * OrderManagementSystem* obj = new OrderManagementSystem();
71 * obj->addOrder(orderId,orderType,price);
72 * obj->modifyOrder(orderId,newPrice);
73 * obj->cancelOrder(orderId);
74 * vector<int> param_4 = obj->getOrdersAtPrice(orderType,price);
75 */
76
1// Maps each orderId to its order type ("buy" / "sell", etc.)
2let orderTypeMap: Map<number, string> = new Map();
3
4// Maps each orderId to its current price
5let priceMap: Map<number, number> = new Map();
6
7// Maps a composite key ("orderType#price") to the list of orderIds at that key
8let priceBucketMap: Map<string, number[]> = new Map();
9
10/**
11 * Builds a composite key from the order type and price.
12 * Used as the lookup key in priceBucketMap.
13 */
14function buildKey(orderType: string, price: number): string {
15    return `${orderType}#${price}`;
16}
17
18/**
19 * Adds a new order to the system.
20 * Records its type and price, then appends it to the corresponding price bucket.
21 */
22function addOrder(orderId: number, orderType: string, price: number): void {
23    orderTypeMap.set(orderId, orderType);
24    priceMap.set(orderId, price);
25
26    const key = buildKey(orderType, price);
27    if (!priceBucketMap.has(key)) {
28        priceBucketMap.set(key, []);
29    }
30    priceBucketMap.get(key)!.push(orderId);
31}
32
33/**
34 * Modifies the price of an existing order.
35 * Removes the order from its old price bucket and moves it to the new one.
36 */
37function modifyOrder(orderId: number, newPrice: number): void {
38    const orderType = orderTypeMap.get(orderId)!;
39    const oldPrice = priceMap.get(orderId)!;
40
41    // Update stored price
42    priceMap.set(orderId, newPrice);
43
44    // Remove the order from the old price bucket
45    const oldKey = buildKey(orderType, oldPrice);
46    const oldList = priceBucketMap.get(oldKey)!;
47    const oldIndex = oldList.indexOf(orderId);
48    if (oldIndex !== -1) {
49        oldList.splice(oldIndex, 1);
50    }
51
52    // Append the order to the new price bucket
53    const newKey = buildKey(orderType, newPrice);
54    if (!priceBucketMap.has(newKey)) {
55        priceBucketMap.set(newKey, []);
56    }
57    priceBucketMap.get(newKey)!.push(orderId);
58}
59
60/**
61 * Cancels (removes) an existing order from the system.
62 * Clears its type/price records and removes it from the price bucket.
63 */
64function cancelOrder(orderId: number): void {
65    const orderType = orderTypeMap.get(orderId)!;
66    const price = priceMap.get(orderId)!;
67
68    // Remove the order's metadata
69    orderTypeMap.delete(orderId);
70    priceMap.delete(orderId);
71
72    // Remove the order from its price bucket
73    const key = buildKey(orderType, price);
74    const list = priceBucketMap.get(key)!;
75    const index = list.indexOf(orderId);
76    if (index !== -1) {
77        list.splice(index, 1);
78    }
79}
80
81/**
82 * Returns the list of orderIds for a given order type and price.
83 * Returns an empty array if no orders match.
84 */
85function getOrdersAtPrice(orderType: string, price: number): number[] {
86    return priceBucketMap.get(buildKey(orderType, price)) ?? [];
87}
88
89/**
90 * Your functions will be called as such:
91 * addOrder(orderId, orderType, price)
92 * modifyOrder(orderId, newPrice)
93 * cancelOrder(orderId)
94 * var param_4 = getOrdersAtPrice(orderType, price)
95 */
96

Time and Space Complexity

  • Time Complexity:

    • addOrder: O(1). Inserting into the self.orders dictionary and appending to the self.t list are both constant-time operations.
    • modifyOrder: O(n), where n is the length of the list at the original (orderType, price) key. Updating the dictionary and appending to the new list are O(1), but the list.remove(orderId) call must scan the list to find and delete the element, costing O(n).
    • cancelOrder: O(n), where n is the length of the list at the corresponding (orderType, price) key. Deleting from the dictionary is O(1), but the list.remove(orderId) operation is O(n).
    • getOrdersAtPrice: O(1). Returning the list at the given key is a constant-time dictionary lookup.

    Since the total number of orders does not exceed 2000, the O(n) removal cost remains efficient enough in practice.

  • Space Complexity: O(m), where m is the total number of orders. Both self.orders and self.t store information proportional to the number of active orders.

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall: Performing remove on the list inside modifyOrder/cancelOrder leads to O(n) time and potential Time Limit Exceeded

The most common pitfall in this design is relying on Python's list.remove(orderId) to delete an order from its (orderType, price) bucket. This method scans the list from the beginning until it finds the target value, giving it O(n) time complexity, where n is the number of orders sharing the same (orderType, price) key.

If the trading platform receives many orders at the same type and price (a very realistic scenario — popular price levels attract many orders), then every modifyOrder and cancelOrder call degrades to a linear scan. A workload with q operations clustered on the same price level can blow up to O(q²), which may cause Time Limit Exceeded on large inputs.

Solution: Replace the per-key list with a per-key set. A set supports O(1) average-time insertion and deletion, eliminating the linear scan entirely. Since the problem explicitly states that the order of returned orderIds does not matter, a set is a perfectly valid replacement.

from collections import defaultdict
from typing import List, Set


class OrderManagementSystem:

    def __init__(self) -> None:
        # Maps orderId -> (orderType, price) for O(1) lookup of any order's details
        self.order_details: dict[int, tuple[str, int]] = {}
        # Maps (orderType, price) -> set of orderIds, allowing O(1) add/remove
        self.orders_by_key: defaultdict[tuple[str, int], Set[int]] = defaultdict(set)

    def addOrder(self, orderId: int, orderType: str, price: int) -> None:
        self.order_details[orderId] = (orderType, price)
        self.orders_by_key[(orderType, price)].add(orderId)

    def modifyOrder(self, orderId: int, newPrice: int) -> None:
        orderType, oldPrice = self.order_details[orderId]
        self.order_details[orderId] = (orderType, newPrice)
        # O(1) removal and insertion instead of O(n) list.remove
        self.orders_by_key[(orderType, oldPrice)].discard(orderId)
        self.orders_by_key[(orderType, newPrice)].add(orderId)

    def cancelOrder(self, orderId: int) -> None:
        orderType, price = self.order_details[orderId]
        del self.order_details[orderId]
        self.orders_by_key[(orderType, price)].discard(orderId)

    def getOrdersAtPrice(self, orderType: str, price: int) -> List[int]:
        # Convert the set to a list since the return type is a list
        return list(self.orders_by_key[(orderType, price)])

This brings modifyOrder and cancelOrder down to O(1) average time, matching the efficiency of addOrder.


Secondary Pitfall: Returning a direct reference to the internal collection

In the original getOrdersAtPrice, returning self.orders_by_key[(orderType, price)] hands the caller a live reference to the internal list. If the caller mutates it (e.g., result.append(999) or result.clear()), the internal state silently corrupts, and subsequent queries return wrong results. This kind of bug is extremely hard to trace because the corruption happens far from where the symptom appears.

Solution: Always return a fresh copy. The set-based version above already does this naturally via list(...), which both converts the type and produces an independent snapshot.


Tertiary Pitfall: Unbounded growth of empty buckets

When orders are repeatedly added and removed across many distinct price levels, orders_by_key accumulates keys whose sets have become empty. Because defaultdict never auto-deletes keys, these empty entries linger forever, slowly inflating memory usage over a long-running session.

Solution: Prune empty buckets after removal. This keeps the dictionary size proportional to the number of active price levels.

def cancelOrder(self, orderId: int) -> None:
    orderType, price = self.order_details[orderId]
    del self.order_details[orderId]
    key = (orderType, price)
    self.orders_by_key[key].discard(orderId)
    # Drop the bucket entirely once it becomes empty
    if not self.orders_by_key[key]:
        del self.orders_by_key[key]

The same pruning logic can be applied to the old bucket in modifyOrder. For LeetCode-style judging this is rarely necessary, but it matters for a realistic, long-lived trading system.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More