Architecting a Multi-Table Restaurant Billing System in Java: An OOP Case Study

The implementation of a restaurant billing platform demonstrates progressive refinement of object-oriented design principles across three iterative versions. The system architecture centers on five primary entities: MenuItem representing culinary offerings, MenuCatalog serving as a centralized registry, OrderEntry capturing transaction line items, Bill aggregating financial data, and DiningTable managing temporal sessions and discount policies.

Foundation Layer: Basic Ordering Logic

The initial architecture establishes price calculation strategies. The MenuItem class encapsulates base pricing and portion multipliers. Pricing follows a tiered algorithm where small portions utilize the base price, medium portions apply a 1.5× coefficient, and large portions apply 2.0×, with Math.round() ensuring integer precision at each step.

public class MenuItem {
    private String identifier;
    private int baseCost;
    private boolean specialtyFlag;

    public int computePortionPrice(int sizeLevel) {
        double scaleFactor = switch (sizeLevel) {
            case 1 -> 1.0;
            case 2 -> 1.5;
            case 3 -> 2.0;
            default -> throw new IllegalArgumentException("Invalid portion size");
        };
        return (int) Math.round(baseCost * scaleFactor);
    }
    
    public boolean isSpecialty() { return specialtyFlag; }
}

class OrderEntry {
    private int sequenceNumber;
    private MenuItem product;
    private int portionSize;
    private int quantity;
    private boolean voided = false;

    public int calculateSubtotal() {
        return voided ? 0 : product.computePortionPrice(portionSize) * quantity;
    }
    
    public void markVoided() { this.voided = true; }
}

class DiningTable {
    private int tableId;
    private LocalDateTime seatedTime;
    private List<OrderEntry> entries = new ArrayList<>();
    
    public double determineDiscountRate() {
        DayOfWeek day = seatedTime.getDayOfWeek();
        int hour = seatedTime.getHour();
        int minute = seatedTime.getMinute();
        
        if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) {
            return isWeekendOperatingHours(hour, minute) ? 1.0 : 0;
        }
        return calculateWeekdayDiscount(hour, minute);
    }
    
    private boolean isWeekendOperatingHours(int h, int m) {
        int minutesSinceMidnight = h * 60 + m;
        return minutesSinceMidnight >= 570 && minutesSinceMidnight <= 1290;
    }
    
    private double calculateWeekdayDiscount(int h, int m) {
        int minutes = h * 60 + m;
        if (minutes >= 630 && minutes <= 870) return 0.6;
        if (minutes >= 1020 && minutes <= 1230) return 0.8;
        return 0;
    }
}

Robustness Layer: Validation and Exception Handling

The fourth iteration introduces comprehensive input validation and business rule enforcement. The parser employs regular expressions to enforce format constraints before object instantiation. Invalid date ranges (outside 2022–2023), malformed phone numbers, and illegal table identifiers (outside 1–55) trigger immediate rejection.

The system handles duplicate table logic through temporal analysis. When identical table numbers appear, the TableManager calculates Duration between seating times. Weekday tables within the same meal period (lunch or dinner) merge their order lists, while weekend tables merge only if seated within 3600 seconds of each other.

Specialty dishes introduce polymorphic pricing. Items marked with type "T" apply a 0.7 multiplier on weekdays regardless of meal period, implemented through strategy pattern delegation rather than conditional branching.

class TableManager {
    private Map<Integer, List<DiningTable>> activeTables = new HashMap<>();
    
    public DiningTable resolveTable(int number, LocalDateTime time) {
        List<DiningTable> existing = activeTables.getOrDefault(number, new ArrayList<>());
        
        for (DiningTable table : existing) {
            if (isSameSession(table.getSeatedTime(), time)) {
                return table;
            }
        }
        
        DiningTable newTable = new DiningTable(number, time);
        activeTables.computeIfAbsent(number, k -> new ArrayList<>()).add(newTable);
        return newTable;
    }
    
    private boolean isSameSession(LocalDateTime t1, LocalDateTime t2) {
        if (!t1.toLocalDate().equals(t2.toLocalDate())) return false;
        
        DayOfWeek day = t1.getDayOfWeek();
        long minutesBetween = Math.abs(ChronoUnit.MINUTES.between(t1, t2));
        
        if (day.getValue() <= 5) {
            boolean bothLunch = isLunchHour(t1) && isLunchHour(t2);
            boolean bothDinner = isDinnerHour(t1) && isDinnerHour(t2);
            return bothLunch || bothDinner;
        }
        return minutesBetween < 60;
    }
    
    private boolean isLunchHour(LocalDateTime t) {
        int mins = t.getHour() * 60 + t.getMinute();
        return mins >= 630 && mins <= 870;
    }
    
    private boolean isDinnerHour(LocalDateTime t) {
        int mins = t.getHour() * 60 + t.getMinute();
        return mins >= 1020 && mins <= 1230;
    }
}

Domain Complexity: Cuisine Taxonomy and Customer Management

The final version introduces culinary domain modeling through enumerated cuisine types: Sichuan (spiciness levels 0–5), Shanxi (acidity 0–4), and Zhejiang (sweetness 0–3). The CuisineProfile class aggregates flavor metrics, calculating weighted averages based on portion quantities to generate descriptors such as "moderately spicy" or "slightly tart."

Customer entities now persist across multiple tables. The CustomerRegistry validates mobile numbers against carrier prefixes (180, 181, 189, 133, 135, 136) using regex ^1(80|81|89|33|35|36)\d{8}$, accumulating total expenditure across all dining sessions for final billing.

Cross-table ordering creates bidirectional record associations. When table A orders for table B, the system generates a proxy entry in A's bill while appending the actual culinary record to B's flavor profile calculations, ensuring accurate taste averaging while attributing payment correct.

enum CuisineType {
    SICHUAN("Spicy", new String[]{"Mild", "Medium", "Hot", "Extra Hot", "Extreme", "Intense"}, 5),
    SHANXI("Acidity", new String[]{"Plain", "Slight", "Moderate", "Strong", "Intense"}, 4),
    ZHEJIANG("Sweetness", new String[]{"Plain", "Slight", "Moderate", "High"}, 3);
    
    final String metricName;
    final String[] gradations;
    final int maxLevel;
    
    CuisineType(String metric, String[] levels, int max) {
        this.metricName = metric;
        this.gradations = levels;
        this.maxLevel = max;
    }
}

class CulinaryRecord extends OrderEntry {
    private CuisineType cuisine;
    private int flavorIntensity;
    
    public boolean validateIntensity() {
        return flavorIntensity >= 0 && flavorIntensity <= cuisine.maxLevel;
    }
    
    public String getFlavorDescriptor() {
        return cuisine.gradations[flavorIntensity];
    }
}

class FlavorAggregator {
    private int spicyTotal = 0, spicyWeight = 0;
    private int acidTotal = 0, acidWeight = 0;
    private int sweetTotal = 0, sweetWeight = 0;
    
    public void accumulate(CulinaryRecord record) {
        switch (record.getCuisine()) {
            case SICHUAN -> {
                spicyTotal += record.getFlavorIntensity() * record.getQuantity();
                spicyWeight += record.getQuantity();
            }
            case SHANXI -> {
                acidTotal += record.getFlavorIntensity() * record.getQuantity();
                acidWeight += record.getQuantity();
            }
            case ZHEJIANG -> {
                sweetTotal += record.getFlavorIntensity() * record.getQuantity();
                sweetWeight += record.getQuantity();
            }
        }
    }
    
    public String generateReport() {
        StringBuilder sb = new StringBuilder();
        if (spicyWeight > 0) {
            int avg = (int) Math.round((double) spicyTotal / spicyWeight);
            sb.append(" Sichuan ").append(spicyWeight).append(" ")
              .append(CuisineType.SICHUAN.gradations[avg]);
        }
        if (acidWeight > 0) {
            int avg = (int) Math.round((double) acidTotal / acidWeight);
            sb.append(" Shanxi ").append(acidWeight).append(" ")
              .append(CuisineType.SHANXI.gradations[avg]);
        }
        if (sweetWeight > 0) {
            int avg = (int) Math.round((double) sweetTotal / sweetWeight);
            sb.append(" Zhejiang ").append(sweetWeight).append(" ")
              .append(CuisineType.ZHEJIANG.gradations[avg]);
        }
        return sb.toString();
    }
}

class CustomerLedger {
    private String patronName;
    private String mobileNumber;
    private int cumulativeSpend = 0;
    
    public void addCharge(int amount) { cumulativeSpend += amount; }
    
    public boolean validatePhone() {
        return mobileNumber.matches("^1(80|81|89|33|35|36)\\d{8}$");
    }
}

Architectural Insights

The progression from basic array storage to ArrayList and HashMap implementations reflects increasing complexity in data relationships. The system employs defensive copying for MenuItem references to prevent external mutation of catalog entries. Temporal calculations utilize java.time API exclusively, avoiding deprecated Date and Calendar classes to ensure thread-safe operations.

State management distinguishes between logical deletion (soft voids with deduplication tracking) and physical removal, preventing double-deletion errors while maintaining audit trails. The rounding strategy applies at three distinct phases: per-unit portion calculation, per-line discount application, and final aggregation, minimizing floating-point drift in monetary calculations.

Tags: java Object-Oriented Programming Restaurant Management System Design Patterns Domain Modeling

Posted on Thu, 03 Sep 2026 16:48:41 +0000 by emilyfrazier