Index

Case Study 2 Online Shopping Cart System

Java Object-Oriented Design

20.1 Managing Products and Inventory

Core Components for Product and Inventory Management

In an online shopping cart system, managing products and inventory forms the backbone of the platform. These components ensure customers can browse available products, view accurate pricing, and purchase items that are in stock. At its core, product and inventory management encompasses:

Modeling these elements thoughtfully in your design enables a robust, scalable system that supports day-to-day operations and growth.

Modeling Products, Categories, Stock, and Prices

Product

The Product class represents an individual item for sale. Key attributes typically include:

Additional fields might include SKU, weight, or supplier info depending on complexity.

Category

Categories organize products into logical groups such as Electronics, Clothing, or Books. The Category class might have:

This structure supports browsing and filtering in the user interface.

Inventory Management

Inventory tracks the stock quantity of each product. This can be represented simply within the Product class or as a separate InventoryItem class if more detail is needed (e.g., location-based stock).

Stock levels must be carefully managed to avoid overselling, especially in concurrent purchase scenarios.

Java Class Examples

Below is a simplified example of the core classes modeling these concepts:

// Category.java
public class Category {
    private final String id;
    private final String name;
    private Category parentCategory;

    public Category(String id, String name) {
        this.id = id;
        this.name = name;
    }

    // Getters and setters omitted for brevity
}

// Product.java
public class Product {
    private final String id;
    private String name;
    private String description;
    private Category category;
    private double price;
    private int stockQuantity;

    public Product(String id, String name, String description, Category category, double price, int stockQuantity) {
        this.id = id;
        this.name = name;
        this.description = description;
        this.category = category;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }

    public synchronized boolean purchase(int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Purchase quantity must be positive");
        }
        if (stockQuantity >= quantity) {
            stockQuantity -= quantity;
            return true;
        }
        return false; // Not enough stock
    }

    public synchronized void restock(int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Restock quantity must be positive");
        }
        stockQuantity += quantity;
    }

    // Getters and setters omitted for brevity
}

This design encapsulates stock management within the Product class. The purchase method ensures stock is decremented only if sufficient units exist, and restock allows replenishing inventory.

Inventory Updates: Purchases and Restocking

When customers place orders, the system must update stock levels accordingly:

The synchronization in these methods (using synchronized) helps prevent race conditions when multiple purchase or restock operations happen concurrently in a multi-threaded environment, a common real-world challenge.

Real-World Considerations: Concurrency and Consistency

In practice, inventory management must account for:

These considerations often push designs toward robust transactional support, event-driven updates, or use of message queues to maintain inventory accuracy and scalability.

Summary

Managing products and inventory in an online shopping cart system requires carefully modeled classes reflecting core domain concepts: products, categories, prices, and stock levels. Encapsulation of inventory updates within Product ensures atomic and safe stock modifications.

While the presented code offers a clear starting point, real-world applications must address concurrency and consistency challenges through additional architectural layers and infrastructure support.

With a strong domain model and understanding of operational nuances, the system will be well-positioned for reliability, extensibility, and smooth customer experience.

Index

20.2 User and Cart Design

Modeling Users and Shopping Carts

In an online shopping cart system, managing users and their carts is essential for delivering a smooth purchasing experience. Users represent the customers interacting with the platform, while shopping carts hold the collection of products that users intend to purchase.

When designing these components, focus on clearly modeling attributes and behaviors for each, keeping in mind that users may be either guest users or registered users. The shopping cart must support common operations like adding/removing items, updating quantities, and calculating the total price.

User Model: Customers and Sessions

The User class encapsulates data and behaviors for customers. Important attributes include:

The design should allow for extensibility to accommodate different user types. For example, a GuestUser subclass might omit authentication details, while a RegisteredUser might include address and payment info.

Shopping Cart Model

The ShoppingCart class maintains a collection of CartItem instances, each representing a product and quantity selected by the user. Key responsibilities include:

This design promotes encapsulation by centralizing cart logic and interactions.

Java Code Examples

Below is a runnable example illustrating the user and cart design, including basic cart operations.

import java.util.*;

// User.java
public class User {
    private final String id;
    private final String name;
    private final String email;
    private final boolean isRegistered;
    private ShoppingCart cart;

    public User(String id, String name, String email, boolean isRegistered) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.isRegistered = isRegistered;
        this.cart = new ShoppingCart();
    }

    public ShoppingCart getCart() {
        return cart;
    }

    // Additional getters and user-specific methods here
}

// CartItem.java
class CartItem {
    private final Product product;
    private int quantity;

    public CartItem(Product product, int quantity) {
        this.product = product;
        this.quantity = quantity;
    }

    public Product getProduct() { return product; }
    public int getQuantity() { return quantity; }
    public void setQuantity(int quantity) { this.quantity = quantity; }

    public double getTotalPrice() {
        return product.getPrice() * quantity;
    }
}

// ShoppingCart.java
class ShoppingCart {
    private final Map<String, CartItem> items = new HashMap<>();

    public void addProduct(Product product, int quantity) {
        if (quantity <= 0) throw new IllegalArgumentException("Quantity must be positive");
        CartItem item = items.get(product.getId());
        if (item == null) {
            items.put(product.getId(), new CartItem(product, quantity));
        } else {
            item.setQuantity(item.getQuantity() + quantity);
        }
    }

    public void removeProduct(String productId) {
        items.remove(productId);
    }

    public void updateProductQuantity(String productId, int quantity) {
        if (quantity <= 0) {
            removeProduct(productId);
        } else {
            CartItem item = items.get(productId);
            if (item != null) {
                item.setQuantity(quantity);
            }
        }
    }

    public double calculateTotal() {
        return items.values().stream()
            .mapToDouble(CartItem::getTotalPrice)
            .sum();
    }

    public void clear() {
        items.clear();
    }

    public void printCartContents() {
        if (items.isEmpty()) {
            System.out.println("Shopping cart is empty.");
            return;
        }
        System.out.println("Shopping Cart Contents:");
        for (CartItem item : items.values()) {
            System.out.printf("- %s (x%d): $%.2f%n",
                item.getProduct().getName(),
                item.getQuantity(),
                item.getTotalPrice());
        }
        System.out.printf("Total: $%.2f%n", calculateTotal());
    }
}

// Product.java (simplified for demo)
class Product {
    private final String id;
    private final String name;
    private final double price;

    public Product(String id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public double getPrice() { return price; }
}

// Demo usage
public class ShoppingCartDemo {
    public static void main(String[] args) {
        Product p1 = new Product("P001", "Wireless Mouse", 25.99);
        Product p2 = new Product("P002", "Mechanical Keyboard", 79.99);

        User user = new User("U1001", "Alice Smith", "alice@example.com", true);

        ShoppingCart cart = user.getCart();
        cart.addProduct(p1, 2);
        cart.addProduct(p2, 1);
        cart.printCartContents();

        cart.updateProductQuantity("P001", 1);
        cart.printCartContents();

        cart.removeProduct("P002");
        cart.printCartContents();
    }
}

This example illustrates key behaviors: adding products to the cart, updating quantities, removing items, and calculating totals. The User holds a reference to their ShoppingCart, modeling the user-session relationship.

Design for Extensibility: Guest vs Registered Users

In a real system, users fall into different categories with varying privileges and data needs:

To support this, consider subclassing User:

public class GuestUser extends User {
    public GuestUser() {
        super(UUID.randomUUID().toString(), "Guest", null, false);
    }
}

public class RegisteredUser extends User {
    private String address;
    // Additional attributes

    public RegisteredUser(String id, String name, String email, String address) {
        super(id, name, email, true);
        this.address = address;
    }

    // Getters and setters for extended info
}

This design enables differentiated behavior while reusing common user-cart functionality.

Managing User Sessions and Cart Persistence

In web applications, user sessions manage state between requests. The shopping cart often persists in the user session or a database. Design considerations include:

Though these involve infrastructure beyond plain OOP, designing clear cart and user abstractions helps integrate with session and persistence mechanisms easily.

Summary

Modeling users and shopping carts requires careful attention to data encapsulation and behaviors such as adding/removing products and calculating totals. By designing flexible user hierarchies, the system can cleanly handle guest and registered users alike.

The sample code demonstrates core operations in a straightforward, maintainable way. Future extensions might include coupon handling, inventory checks at add-to-cart time, or cart expiration policies.

Effective design here enhances user experience and supports evolving business requirements.

Index

20.3 Applying Patterns and Principles

Applying SOLID Principles and Design Patterns

In designing an online shopping cart system, adhering to established object-oriented principles and leveraging design patterns is key to building flexible, maintainable, and extensible software. Throughout the system, SOLID principles guide the structure, while classic design patterns solve common challenges elegantly.

Where SOLID Principles Apply

Strategy Pattern: Flexible Payment Processing

One classic example is the Strategy pattern to encapsulate payment algorithms. The system defines a PaymentStrategy interface:

public interface PaymentStrategy {
    void pay(double amount);
}

Concrete implementations for different payment methods—credit card, PayPal, or gift card—implement this interface:

public class CreditCardPayment implements PaymentStrategy {
    public void pay(double amount) {
        // process credit card payment
        System.out.println("Paid $" + amount + " with credit card.");
    }
}

public class PayPalPayment implements PaymentStrategy {
    public void pay(double amount) {
        // process PayPal payment
        System.out.println("Paid $" + amount + " via PayPal.");
    }
}

The shopping cart or checkout class can then accept any PaymentStrategy instance, allowing seamless addition of new payment methods without modifying existing code:

public class CheckoutService {
    private PaymentStrategy paymentStrategy;

    public CheckoutService(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void checkout(double amount) {
        paymentStrategy.pay(amount);
    }
}
Click to view full runnable Code

// PaymentStrategy interface
public interface PaymentStrategy {
    void pay(double amount);
}

// Concrete strategy: Credit Card payment
public class CreditCardPayment implements PaymentStrategy {
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " with credit card.");
    }
}

// Concrete strategy: PayPal payment
public class PayPalPayment implements PaymentStrategy {
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " via PayPal.");
    }
}

// Checkout service using a PaymentStrategy
public class CheckoutService {
    private PaymentStrategy paymentStrategy;

    public CheckoutService(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void checkout(double amount) {
        paymentStrategy.pay(amount);
    }
}

// Demo usage
public class StrategyDemo {
    public static void main(String[] args) {
        CheckoutService checkout1 = new CheckoutService(new CreditCardPayment());
        checkout1.checkout(100.00);

        CheckoutService checkout2 = new CheckoutService(new PayPalPayment());
        checkout2.checkout(55.50);
    }
}

Benefits:

Observer Pattern: Event-Driven Notifications

The Observer pattern supports sending notifications—such as order confirmation emails or SMS alerts—when certain events occur (e.g., successful payment or item shipment).

Define an abstract Notifier interface:

public interface Notifier {
    void update(String message);
}

Concrete observers implement this interface:

public class EmailNotifier implements Notifier {
    public void update(String message) {
        System.out.println("Sending email: " + message);
    }
}

public class SMSNotifier implements Notifier {
    public void update(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

The subject class (e.g., Order) maintains a list of observers and notifies them on events:

import java.util.*;

public class Order {
    private List<Notifier> notifiers = new ArrayList<>();

    public void addNotifier(Notifier notifier) {
        notifiers.add(notifier);
    }

    public void completeOrder() {
        // Order completion logic
        notifyAllObservers("Order completed successfully.");
    }

    private void notifyAllObservers(String message) {
        for (Notifier notifier : notifiers) {
            notifier.update(message);
        }
    }
}
Click to view full runnable Code

import java.util.*;

// Observer interface
public interface Notifier {
    void update(String message);
}

// Concrete observer: Email notification
public class EmailNotifier implements Notifier {
    public void update(String message) {
        System.out.println("Sending email: " + message);
    }
}

// Concrete observer: SMS notification
public class SMSNotifier implements Notifier {
    public void update(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

// Subject class
public class Order {
    private List<Notifier> notifiers = new ArrayList<>();

    public void addNotifier(Notifier notifier) {
        notifiers.add(notifier);
    }

    public void completeOrder() {
        // Order completion logic
        notifyAllObservers("Order completed successfully.");
    }

    private void notifyAllObservers(String message) {
        for (Notifier notifier : notifiers) {
            notifier.update(message);
        }
    }
}

// Demo usage
public class ObserverDemo {
    public static void main(String[] args) {
        Order order = new Order();
        order.addNotifier(new EmailNotifier());
        order.addNotifier(new SMSNotifier());

        order.completeOrder();
    }
}

Benefits:

Factory Pattern: Simplifying Object Creation

When creating products or users, the Factory pattern can encapsulate complex instantiation logic:

public class UserFactory {
    public static User createUser(String type, String name, String email) {
        if ("guest".equalsIgnoreCase(type)) {
            return new GuestUser();
        } else if ("registered".equalsIgnoreCase(type)) {
            return new RegisteredUser(name, email);
        }
        throw new IllegalArgumentException("Unknown user type");
    }
}

This approach centralizes creation logic, reducing duplication and adhering to SRP.

Decorator Pattern: Enhancing Cart Features Dynamically

The Decorator pattern allows dynamically adding responsibilities to objects. For example, you might add a feature to apply discounts to a cart without modifying the original ShoppingCart class:

public interface Cart {
    double calculateTotal();
}

public class BasicCart implements Cart {
    // existing cart implementation
}

public class DiscountedCart implements Cart {
    private Cart wrappedCart;
    private double discountRate;

    public DiscountedCart(Cart cart, double discountRate) {
        this.wrappedCart = cart;
        this.discountRate = discountRate;
    }

    @Override
    public double calculateTotal() {
        double baseTotal = wrappedCart.calculateTotal();
        return baseTotal * (1 - discountRate);
    }
}

Benefits:

How These Patterns Improve the System

Simplified UML Diagram

+--------------------+         +--------------------+
|  PaymentStrategy   |<--------|  CreditCardPayment|
|  <<interface>>     |         +-------------------+
|  + pay(amount)     |         |  + pay(amount)    |
+--------------------+         +--------------------+
          ^
          |
+--------------------+
|  PayPalPayment     |
+--------------------+

+-----------------+      subscribes     +---------------+
|    Order        |-------------------->|   Notifier    |
+-----------------+                     +---------------+
| +completeOrder()|                     | +update(msg)  |
+-----------------+                     +---------------+
                                          ^           ^
                                          |           |
                                +---------------+  +------------+
                                | EmailNotifier |  | SMSNotifier|
                                +---------------+  +------------+

Summary

By applying SOLID principles and design patterns like Strategy, Observer, Factory, and Decorator, the shopping cart system achieves modularity and adaptability. These patterns allow for clean separation of concerns, ease of extension, and clearer communication among developers — all essential qualities for robust software development.

Index

20.4 Testing and Validation

Testing is a critical activity in software development, ensuring that a system functions correctly, reliably, and as intended. For an online shopping cart system, thorough testing is essential because it directly impacts user experience, business operations, and trust.

Importance of Testing

The complexity of a shopping cart system — involving user sessions, inventory updates, payment processing, and more — makes it vulnerable to errors. Testing reduces the risk of bugs, prevents regressions during changes, and guarantees that the system meets its functional and non-functional requirements. Automated tests accelerate development by providing quick feedback, enabling safer refactoring, and improving code quality.

Unit Testing Core Components

Unit tests focus on small, isolated pieces of code, such as classes or methods, verifying their behavior independently. In the shopping cart system, key components to unit test include:

Unit testing frameworks like JUnit are widely used in Java to write and run such tests.

Example JUnit Test Case

Consider testing the ShoppingCart class’s ability to add products and calculate the total price:

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class ShoppingCartTest {

    private ShoppingCart cart;
    private Product apple;
    private Product orange;

    @BeforeEach
    public void setup() {
        cart = new ShoppingCart();
        apple = new Product("Apple", 1.0);
        orange = new Product("Orange", 1.5);
    }

    @Test
    public void testAddItemAndCalculateTotal() {
        cart.addItem(apple, 3); // 3 apples
        cart.addItem(orange, 2); // 2 oranges
        double expectedTotal = 3 * 1.0 + 2 * 1.5;
        assertEquals(expectedTotal, cart.calculateTotal());
    }

    @Test
    public void testRemoveItem() {
        cart.addItem(apple, 2);
        cart.removeItem(apple);
        assertEquals(0, cart.calculateTotal());
    }
}

This example uses the JUnit 5 framework, demonstrating setup, test annotations, and assertions to verify behavior.

Validation Strategies

Validating inputs and business rules is equally important. Validation helps catch errors early and enforce correct usage. Some common validation tasks in the shopping cart system include:

Validation can be implemented using explicit checks in methods or using validation frameworks like Hibernate Validator (JSR-380).

Example validation snippet:

public void addItem(Product product, int quantity) {
    if (quantity <= 0) {
        throw new IllegalArgumentException("Quantity must be positive.");
    }
    if (!inventory.isInStock(product, quantity)) {
        throw new IllegalStateException("Insufficient stock.");
    }
    // add item to cart
}

Best Practices and Tools

Popular tools in Java ecosystem include:

Summary

Testing and validation form the backbone of a reliable online shopping cart system. By writing comprehensive unit tests, validating inputs rigorously, and adopting best practices, developers ensure the system behaves correctly and is robust against changes. This not only improves user trust but also eases ongoing maintenance and feature enhancement.

Index