3822. Design Order Management System 🔒
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 givenorderId,orderType, andprice. It is guaranteed that theorderIdis unique.void modifyOrder(int orderId, int newPrice): Changes thepriceof an existing order tonewPrice. 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 theorderIds of all active orders that match both the givenorderTypeandprice. If no such orders exist, return an empty list.
Note: The order of the returned orderIds does not matter.
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.
Designing an order management system with add/modify/cancel/get-operations at-price requires hash tables and ordered mappings for efficient lookup.
Open in FlowchartIntuition
The key challenge in this problem is supporting two different kinds of lookups efficiently:
-
Given an
orderId, we need to quickly find its currentorderTypeandprice. This is needed bymodifyOrderandcancelOrder, since they only receive theorderIdbut must locate the order's existing attributes to update or remove it. -
Given an
(orderType, price)pair, we need to quickly find all matching active orders. This is exactly whatgetOrdersAtPricerequires.
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 inordersand append its ID to the list intunder the key(orderType, price). - For
modifyOrder, we useordersto look up the old(orderType, price), remove the ID from that old bucket int, then add it to the new bucket for(orderType, newPrice), and finally updateorders. - For
cancelOrder, we again look up the order's attributes fromorders, remove its ID from the matching bucket int, and delete it fromorders. - For
getOrdersAtPrice, we simply return the list stored under(orderType, price)int.
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 toordersby settingorders[orderId] = (orderType, price), and append theorderIdto the corresponding list intunder the key(orderType, price). -
modifyOrder(orderId, newPrice): We first retrieve the order type and old price fromorders. Then we update the order's price information by settingorders[orderId] = (orderType, newPrice). Next, we remove theorderIdfrom the list intkeyed 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 fromorders, delete the order fromorders, and then remove theorderIdfrom the corresponding list intunder the key(orderType, price). -
getOrdersAtPrice(orderType, price): We directly return the list of order IDs corresponding to the key(orderType, price)int. Sincetis adefaultdict(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
1from the old buckett[("buy", 100)]. - Add
1to the new buckett[("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
2from the buckett[("buy", 100)](it becomes empty). - Delete
2fromorders.
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)])
651class 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 */
861class 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 */
761// 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 */
96Time and Space Complexity
-
Time Complexity:
addOrder:O(1). Inserting into theself.ordersdictionary and appending to theself.tlist are both constant-time operations.modifyOrder:O(n), wherenis the length of the list at the original(orderType, price)key. Updating the dictionary and appending to the new list areO(1), but thelist.remove(orderId)call must scan the list to find and delete the element, costingO(n).cancelOrder:O(n), wherenis the length of the list at the corresponding(orderType, price)key. Deleting from the dictionary isO(1), but thelist.remove(orderId)operation isO(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, theO(n)removal cost remains efficient enough in practice. -
Space Complexity:
O(m), wheremis the total number of orders. Bothself.ordersandself.tstore 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 RoadmapWhat is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity describes how the time needed
Want a Structured Path to Master System Design Too? Don’t Miss This!