LLD: Architecting a Real-Time Inventory Management System for E-Commerce
Low Level Design
System Design

LLD: Architecting a Real-Time Inventory Management System for E-Commerce

S

Shivam Chauhan

about 1 month ago

Ever wondered how Amazon, Flipkart, or your favourite online store keeps track of millions of items, ensuring they don't sell something they don't have? It's all about a robust, real-time inventory management system. Today, we're diving deep into the low-level design (LLD) of such a system.

Why Real-Time Inventory Matters

In the fast-paced world of e-commerce, accuracy and speed are key. Imagine this: a customer adds the last available item to their cart, but before they can checkout, someone else snatches it up. A frustrating out-of-stock message appears, leading to a lost sale and a potentially unhappy customer.

A real-time inventory system prevents this by:

  • Accurate Stock Levels: Displaying the most up-to-date quantity of each item.
  • Preventing Overselling: Ensuring you don't sell more than what's in stock.
  • Improving Customer Experience: Reducing frustration and building trust.
  • Optimising Operations: Enabling better forecasting and procurement decisions.

So, how do we build such a system? Let's get into the nitty-gritty.

Core Components

Our real-time inventory system will consist of these key components:

  • Inventory Database: Stores the current stock levels for each product. This could be a relational database (like PostgreSQL) or a NoSQL database (like Cassandra), depending on your scaling needs.
  • Inventory Service: Provides APIs for updating and querying inventory levels. This service acts as an intermediary between the application and the database.
  • Message Queue: A messaging system (like RabbitMQ or Amazon MQ) that facilitates asynchronous communication between different services. This is crucial for handling high volumes of updates without slowing down the application.
  • Cache: An in-memory data store (like Redis or Memcached) that stores frequently accessed inventory data for faster retrieval.

Here’s a basic diagram to illustrate the architecture:

Drag: Pan canvas

Detailed Design

Let's break down each component and its responsibilities.

1. Inventory Database

  • Schema: A products table with columns like product_id, name, description, and quantity.
sql
CREATE TABLE products (
    product_id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    quantity INTEGER NOT NULL
);
  • Considerations: Choose a database that supports ACID transactions to ensure data consistency. For high-volume scenarios, consider using sharding or partitioning to distribute the load.

2. Inventory Service

  • APIs:

    • GET /inventory/{product_id}: Retrieves the current quantity of a product.
    • POST /inventory/{product_id}/add: Adds quantity to a product's inventory.
    • POST /inventory/{product_id}/subtract: Subtracts quantity from a product's inventory.
  • Implementation: The service should handle authentication, authorisation, and input validation. It should also interact with the cache and the message queue.

java
@RestController
@RequestMapping("/inventory")
public class InventoryController {

    @Autowired
    private InventoryService inventoryService;

    @GetMapping("/{productId}")
    public ResponseEntity<Integer> getInventory(@PathVariable UUID productId) {
        int quantity = inventoryService.getInventory(productId);
        return ResponseEntity.ok(quantity);
    }

    @PostMapping("/{productId}/add")
    public ResponseEntity<Void> addInventory(@PathVariable UUID productId, @RequestParam int quantity) {
        inventoryService.addInventory(productId, quantity);
        return ResponseEntity.ok().build();
    }

    @PostMapping("/{productId}/subtract")
    public ResponseEntity<Void> subtractInventory(@PathVariable UUID productId, @RequestParam int quantity) {
        inventoryService.subtractInventory(productId, quantity);
        return ResponseEntity.ok().build();
    }
}

3. Message Queue (RabbitMQ)

  • Purpose: Decouples the inventory updates from the main application flow. When an order is placed or inventory is adjusted, a message is published to the queue.

  • Configuration: Configure the queue for high availability and durability to prevent message loss.

  • Example: Publishing a message when an order is placed:

java
@Service
public class OrderService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private Queue inventoryUpdateQueue;

    public void placeOrder(Order order) {
        // ... other order processing logic ...

        // Publish message to update inventory
        rabbitTemplate.convertAndSend(inventoryUpdateQueue.getName(), new InventoryUpdateMessage(order.getProductId(), order.getQuantity()));
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class InventoryUpdateMessage {
    private UUID productId;
    private int quantity;
}

4. Cache (Redis)

  • Strategy: Use a read-through/write-through cache. When data is requested, the cache is checked first. If the data is not present (cache miss), it's retrieved from the database and stored in the cache.

  • Invalidation: Implement a mechanism to invalidate the cache when inventory is updated to ensure data consistency.

java
@Service
public class InventoryService {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    @Autowired
    private ProductRepository productRepository;

    public int getInventory(UUID productId) {
        String key = "inventory:" + productId.toString();
        Integer quantity = redisTemplate.opsForValue().get(key);
        if (quantity == null) {
            // Cache miss, fetch from database
            Product product = productRepository.findById(productId)
                    .orElseThrow(() -> new IllegalArgumentException("Product not found"));
            quantity = product.getQuantity();
            redisTemplate.opsForValue().set(key, quantity);
        }
        return quantity;
    }

    public void addInventory(UUID productId, int quantity) {
        Product product = productRepository.findById(productId)
                .orElseThrow(() -> new IllegalArgumentException("Product not found"));
        product.setQuantity(product.getQuantity() + quantity);
        productRepository.save(product);

        // Invalidate cache
        String key = "inventory:" + productId.toString();
        redisTemplate.delete(key);
    }

    public void subtractInventory(UUID productId, int quantity) {
        Product product = productRepository.findById(productId)
                .orElseThrow(() -> new IllegalArgumentException("Product not found"));
        if (product.getQuantity() < quantity) {
            throw new IllegalArgumentException("Insufficient stock");
        }
        product.setQuantity(product.getQuantity() - quantity);
        productRepository.save(product);

        // Invalidate cache
        String key = "inventory:" + productId.toString();
        redisTemplate.delete(key);
    }
}

Ensuring Consistency

Data consistency is paramount. Here are some strategies:

  • ACID Transactions: Use database transactions to ensure that updates are atomic, consistent, isolated, and durable.
  • Optimistic Locking: Use a version number or timestamp to detect and prevent concurrent updates.
  • Idempotency: Design your update operations to be idempotent, meaning that they can be applied multiple times without changing the result.

Scaling the System

As your e-commerce platform grows, you'll need to scale your inventory system. Here are some techniques:

  • Horizontal Scaling: Add more instances of the Inventory Service and the database.
  • Sharding: Partition the database based on product categories or other criteria.
  • Load Balancing: Distribute traffic evenly across multiple instances of the Inventory Service.
  • Caching: Use a distributed cache like Redis Cluster to handle a large volume of read requests.

FAQs

1. What database should I use?

  • For smaller applications, a relational database like PostgreSQL is a good choice.
  • For high-volume, scalable applications, consider a NoSQL database like Cassandra.

2. How do I handle inventory updates during peak hours?

  • Use a message queue to decouple the updates from the main application flow.
  • Scale the Inventory Service and the database to handle the increased load.

3. How do I monitor the system?

  • Use monitoring tools like Prometheus and Grafana to track key metrics such as request latency, error rates, and database performance.

4. How does Coudo AI help in understanding these concepts?

  • Coudo AI offers various problems related to system design and low-level design, including scenarios that require real-time updates and consistency. Check out Coudo AI to practice and deepen your understanding.

Wrapping Up

Architecting a real-time inventory management system for e-commerce is a complex but crucial task. By understanding the core components, implementing proper consistency mechanisms, and designing for scalability, you can build a system that meets the demands of your growing business. Remember to practice your skills and explore more problems on platforms like Coudo AI to become a proficient system designer.

Keep it real, keep it fresh, and keep it engaging! And remember, the key to mastering LLD is continuous learning and practice.\n\n

About the Author

S

Shivam Chauhan

Sharing insights about system design and coding practices.