Swapping DB & Payment Gateway in Tests
`OrderService` directly instantiates concrete `PostgresOrderRepo` and `RazorpayGateway` inside its constructor. Because domain policy is tightly coupled to low-level SQL drivers and third-party HTTP SDKs, unit tests require live PostgreSQL databases and internet credentials, running slowly and flaking in CI (**Dependency Inversion Principle (DIP)** violation).
Because OrderService hardcodes PostgresOrderRepo and RazorpayGateway, tests cannot run in isolation. A flaky network or downed staging database breaks the entire CI pipeline.
High-level modules must not depend on low-level modules; both must depend on abstractions.
Instead of OrderService creating concrete dependencies, we define two pure virtual interfaces: OrderRepository and PaymentGateway. OrderService depends only on these abstract contracts, injected through its constructor. In production, we inject PostgresOrderRepo and RazorpayGateway; in unit tests, we inject InMemoryOrderRepo and FakePaymentGateway.
In Sprint 5 (A5), we decoupled vehicle instantiation with Factory Method so FulfillmentService dispatches delivery partners through abstract interfaces.
We need to make our core OrderService enterprise-ready and thoroughly testable by running fast, deterministic unit test suites in CI without depending on live cloud infrastructure.
OrderService directly instantiates concrete PostgresOrderRepo and RazorpayGateway inside its constructor. Because domain policy is tightly coupled to low-level SQL drivers and third-party HTTP SDKs, unit tests require live PostgreSQL databases and internet credentials, running slowly and flaking in CI (Dependency Inversion Principle (DIP) violation).
By inverting dependencies with pure virtual interfaces (OrderRepository, PaymentGateway) and constructor injection, production injects PostgreSQL and Razorpay while unit tests inject lightning-fast in-memory test doubles. Next in Sprint 7 (A7 Β· LSP & ISP), our expanding partner ecosystem breaks down when new cloud kitchens and dine-in partners are forced to implement bloated fat interfaces with dummy 'Not supported!' exceptions.
The product ticket
Our OrderService directly instantiates PostgresOrderRepo and RazorpayGateway inside its constructor. As a result, running unit tests requires a live PostgreSQL instance on port 5432 and live internet credentials for Razorpay, causing test runs to take seconds and fail when offline.
- β’
Decouple
OrderServicefrom concrete database drivers and network payment SDKs. - β’
Enable 100% in-memory unit tests that execute in microseconds without starting PostgreSQL or calling Razorpay.
- β’
Allow swapping infrastructure backends (e.g. PostgreSQL in production, hash map in tests, DynamoDB for regional expansion) without modifying order processing logic.
Next month we are launching in Southeast Asia with local payment rails (GrabPay, GoPay) and AWS DynamoDB. The core checkout rules must remain completely untouched when switching storage engines or payment providers.
Step 1 β The naive code & why it breaks
The initial implementation instantiates concrete PostgresOrderRepo and RazorpayGateway objects directly inside OrderService.
#include <optional>
#include <print>
#include <string>
struct Order {
std::string id;
std::string customerId;
int amountRupees;
};
// Low-level database detail directly coupled to Postgres
class PostgresOrderRepo {
public:
void save(const Order& order) {
std::println("[DB:Postgres] Connecting to postgres://prod-db:5432/orders... Saved order {}", order.id);
}
std::optional<Order> findById(const std::string& orderId) {
std::println("[DB:Postgres] SELECT * FROM orders WHERE id = '{}'", orderId);
return Order{orderId, "cust_live", 450};
}
};
// Low-level network detail directly coupled to Razorpay HTTP API
class RazorpayGateway {
public:
bool charge(const std::string& customerId, int amountRupees) {
std::println("[GATEWAY:Razorpay] POST https://api.razorpay.com/v1/charges for {} (Rs.{})",
customerId, amountRupees);
return true;
}
};
// High-level policy directly instantiates concrete database and payment classes!
class OrderService {
PostgresOrderRepo repo_; // Direct tight coupling
RazorpayGateway gateway_; // Direct tight coupling
public:
OrderService() : repo_(), gateway_() {}
bool checkout(const Order& order) {
std::println("--- Processing Checkout for Order {} ---", order.id);
if (!gateway_.charge(order.customerId, order.amountRupees)) {
std::println("[OrderService] Payment failed for order {}", order.id);
return false;
}
repo_.save(order);
std::println("[OrderService] Order {} placed successfully!", order.id);
return true;
}
};
int main() {
OrderService service;
Order o1{"ORD-501", "cust_101", 450};
service.checkout(o1);
return 0;
}Because OrderService hardcodes PostgresOrderRepo and RazorpayGateway, tests cannot run in isolation. A flaky network or downed staging database breaks the entire CI pipeline.
The core business policy ('validate, charge, then persist order') directly references SQL connection strings and vendor HTTP SDKs β violating the Dependency Inversion Principle (DIP).
Swapping PostgreSQL for DynamoDB or Razorpay for Stripe requires editing and re-compiling OrderService, risking regressions in battle-tested checkout rules.
Step 2 β The fix: Dependency Inversion Principle (DIP) + Constructor Injection
High-level modules must not depend on low-level modules; both must depend on abstractions.
Instead of OrderService creating concrete dependencies, we define two pure virtual interfaces: OrderRepository and PaymentGateway. OrderService depends only on these abstract contracts, injected through its constructor. In production, we inject PostgresOrderRepo and RazorpayGateway; in unit tests, we inject InMemoryOrderRepo and FakePaymentGateway.
OrderService β orchestrates the checkout process; depends only on OrderRepository and PaymentGateway interfaces.
OrderRepository β pure virtual interface declaring save(order) and findById(id) with a virtual destructor.
PaymentGateway β pure virtual interface declaring charge(customerId, amountRupees).
PostgresOrderRepo and RazorpayGateway β implement the interfaces with real SQL queries and vendor network calls.
InMemoryOrderRepo and FakePaymentGateway β zero-dependency test doubles that store data in memory and simulate network responses.
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.