Splitting a God OrderService
We dump cart validation, tax math, SQL persistence, and receipt formatting into a single `OrderService` God class. When finance changes GST rules, logistics updates delivery fee slabs, and infra modifies DB schemas in the same week, multiple squads collide in the exact same class (**Single Responsibility Principle (SRP)** violation).
A class should have one, and only one, reason to change. OrderService changes when validation rules change, tax rules change, delivery pricing changes, DB schema changes, or SMS copy changes.
A class should have only one reason to change, meaning it should encapsulate exactly one cohesive actor-facing responsibility.
We decompose OrderService into dedicated, cohesive collaborators: OrderValidator handles invariants, PriceCalculator handles tax and fee arithmetic, OrderRepository handles storage, and ReceiptFormatter handles text templating. OrderService becomes a thin coordinator whose only responsibility is orchestrating the sequence of steps.
In Sprint 1 (A1), we refactored Checkout to accept any PaymentStrategy (UpiPayment, CardPayment, WalletPayment). Now we have a paid PaymentReceipt and need to process the placed order.
We are building the post-payment order flow: validating cart items, calculating 5% GST and delivery fees, persisting the order to storage, and formatting the customer invoice/SMS receipt.
We dump cart validation, tax math, SQL persistence, and receipt formatting into a single OrderService God class. When finance changes GST rules, logistics updates delivery fee slabs, and infra modifies DB schemas in the same week, multiple squads collide in the exact same class (Single Responsibility Principle (SRP) violation).
With OrderValidator, PriceCalculator, OrderRepository, and ReceiptFormatter cleanly separated, our order is persisted cleanly. Next in Sprint 3 (A3 · Observer), the restaurant kitchen display, delivery rider app, and customer push service all need real-time notifications when an order is placed.
The product ticket
Our food delivery MVP placed orders using a single OrderService::placeOrder() method. It validates cart items, calculates 5% GST and delivery fees, charges payment, saves the record to the database, and sends an SMS receipt.
- •
Validate that the cart is non-empty, item prices and quantities are positive, and delivery address is provided.
- •
Calculate order subtotal, 5% GST, and distance-tiered delivery fee (Rs. 30 up to 5 km, Rs. 50 beyond).
- •
Persist the order to storage and format an SMS receipt with complete breakdown for the customer.
Every product sprint edits OrderService. When the finance team changes GST rules, logistics updates delivery fee slabs, the database team changes table schemas, or marketing tweaks the SMS template, all four squads edit the exact same file. Merge collisions are frequent, and an SMS formatting bug recently broke order placement entirely.
Step 1 — The naive code & why it breaks
Everything is stuffed into one monolithic NaiveOrderService class where placeOrder() handles five distinct operational stages sequentially.
#include <print>
#include <string>
#include <vector>
struct OrderItem {
std::string name;
int priceRupees;
int quantity;
};
// Naive OrderService: one monolithic class handles validation, pricing, taxes,
// payments, database storage, and SMS notifications in a single placeOrder() method.
class NaiveOrderService {
std::vector<std::string> savedOrders_;
public:
bool placeOrder(const std::string& orderId,
const std::string& customerId,
const std::string& address,
double distanceKm,
const std::vector<OrderItem>& items,
const std::string& paymentMethod) {
// 1. Validation logic
if (items.empty()) {
std::println("Error: Cart cannot be empty");
return false;
}
if (address.empty()) {
std::println("Error: Delivery address cannot be blank");
return false;
}
for (const auto& item : items) {
if (item.quantity <= 0 || item.priceRupees <= 0) {
std::println("Error: Invalid item quantity or price");
return false;
}
}
// 2. Pricing & Tax calculation
int subtotal = 0;
for (const auto& item : items) {
subtotal += item.priceRupees * item.quantity;
}
int gst = (subtotal * 5) / 100; // 5% GST
int deliveryFee = (distanceKm <= 5.0) ? 30 : 50;
int grandTotal = subtotal + gst + deliveryFee;
// 3. Payment charging
if (paymentMethod.empty()) {
std::println("Error: Payment method missing");
return false;
}
std::println("[PAYMENT] Charged Rs.{} via {}", grandTotal, paymentMethod);
// 4. Persistence / Database saving
std::string record = std::format("ORDER#{}|CUST#{}|AMT#{}", orderId, customerId, grandTotal);
savedOrders_.push_back(record);
std::println("[DB] Saved order record: {}", record);
// 5. Receipt & SMS notification
std::string sms = std::format("Order {} placed! Subtotal: Rs.{}, GST: Rs.{}, Delivery: Rs.{}. Total: Rs.{}. Delivering to: {}",
orderId, subtotal, gst, deliveryFee, grandTotal, address);
std::println("[SMS] Sent to customer {}: {}", customerId, sms);
return true;
}
};
int main() {
NaiveOrderService service;
std::vector<OrderItem> cart = {
{"Paneer Butter Masala", 280, 1},
{"Garlic Naan", 40, 2}
};
service.placeOrder("ORD-201", "CUST-42", "Flat 302, Green Glen Layout, Bengaluru", 4.2, cart, "UPI");
}A class should have one, and only one, reason to change. OrderService changes when validation rules change, tax rules change, delivery pricing changes, DB schema changes, or SMS copy changes.
Engineers from Finance, Logistics, and Core Checkout collide in Git on the same file. A typo in string formatting or tax math can crash core order persistence.
You cannot test whether delivery fee tiers work at 4.9 km vs 5.1 km without supplying valid customer addresses, triggering fake payment charges, and printing fake SMS messages.
Step 2 — The fix: Single Responsibility Principle (SRP)
A class should have only one reason to change, meaning it should encapsulate exactly one cohesive actor-facing responsibility.
We decompose OrderService into dedicated, cohesive collaborators: OrderValidator handles invariants, PriceCalculator handles tax and fee arithmetic, OrderRepository handles storage, and ReceiptFormatter handles text templating. OrderService becomes a thin coordinator whose only responsibility is orchestrating the sequence of steps.
OrderValidator — checks cart emptiness, address presence, and item non-negativity.
PriceCalculator — calculates subtotal, 5% GST, and distance-based delivery fees.
OrderRepository — manages persistence and order retrieval by ID.
ReceiptFormatter — constructs human-readable SMS and receipt summaries.
OrderService — coordinates the flow between validator, calculator, repository, and formatter.
Unlock Full UML Diagram & C++23 Solution
The interactive UML architecture diagrams, refactored C++23 code, real-world extensions, and interview Q&A are part of Brute2Best Pro. All 7 foundation chapters are 100% free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.