Reading Class Diagrams & Code Smells
In Low-Level Design interviews and team architectural reviews, UML class diagrams are the universal visual language, while code smells are the warning sirens indicating architectural decay. When candidates fail LLD interviews, it is rarely because they cannot write a loop; it is because they draw ambiguous diagrams that don't translate to real code, or fail to spot the structural code smells that scream for refactoring. Translating every UML arrow directly to exact C++ semanticsβand mapping every code smell to a violated SOLID principleβturns fuzzy design intuition into a repeatable engineering discipline.
The Two Pillars of Structural Design:
-
The 5 UML Relationships in C++:
- Implements (βββ·): Contract fulfillment (
class Impl : public IInterface). Pure virtual interface with zero member state. - Inherits (βββ·): Code reuse / subtyping (
class Derived : public Base). Shared state and non-virtual logic. - Composes (ββββΆ): Strong exclusive ownership (
std::unique_ptr<Part>or direct value memberPart part_). The part's lifetime is strictly bounded by the whole. - Aggregates (ββββΆ): Shared or borrowed association (
std::shared_ptr<Part>orPart*). The part exists independently and can outlive the container. - Depends (βββΆ): Transient 'uses-a' relationship (
void run(const Request& req)). Passed as a method parameter or constructed locally.
- Implements (βββ·): Contract fulfillment (
-
The Code Smell to SOLID Diagnostic Matrix:
- God Class β Violates Single Responsibility Principle (SRP).
- Switch on Type β Violates Open-Closed Principle (OCP).
- Refused Bequest β Violates Liskov Substitution Principle (LSP).
- Fat Interface β Violates Interface Segregation Principle (ISP).
- Hardcoded
newβ Violates Dependency Inversion Principle (DIP). - Shotgun Surgery β High coupling & low cohesion across module boundaries.
Why this matters in LLD interviews
In Low-Level Design interviews and team architectural reviews, UML class diagrams are the universal visual language, while code smells are the warning sirens indicating architectural decay. When candidates fail LLD interviews, it is rarely because they cannot write a loop; it is because they draw ambiguous diagrams that don't translate to real code, or fail to spot the structural code smells that scream for refactoring. Translating every UML arrow directly to exact C++ semanticsβand mapping every code smell to a violated SOLID principleβturns fuzzy design intuition into a repeatable engineering discipline.
The Two Pillars of Structural Design:
-
The 5 UML Relationships in C++:
- Implements (βββ·): Contract fulfillment (
class Impl : public IInterface). Pure virtual interface with zero member state. - Inherits (βββ·): Code reuse / subtyping (
class Derived : public Base). Shared state and non-virtual logic. - Composes (ββββΆ): Strong exclusive ownership (
std::unique_ptr<Part>or direct value memberPart part_). The part's lifetime is strictly bounded by the whole. - Aggregates (ββββΆ): Shared or borrowed association (
std::shared_ptr<Part>orPart*). The part exists independently and can outlive the container. - Depends (βββΆ): Transient 'uses-a' relationship (
void run(const Request& req)). Passed as a method parameter or constructed locally.
- Implements (βββ·): Contract fulfillment (
-
The Code Smell to SOLID Diagnostic Matrix:
- God Class β Violates Single Responsibility Principle (SRP).
- Switch on Type β Violates Open-Closed Principle (OCP).
- Refused Bequest β Violates Liskov Substitution Principle (LSP).
- Fat Interface β Violates Interface Segregation Principle (ISP).
- Hardcoded
newβ Violates Dependency Inversion Principle (DIP). - Shotgun Surgery β High coupling & low cohesion across module boundaries.
Core concepts
2.1Mapping the 5 UML Arrows to Concrete C++ Code
A class diagram is useless if it cannot be translated directly into code. When an interviewer points at an arrow, you must state its exact C++ representation:
- Realization / Implements (βββ·): Declared with
class Concrete : public IInterface. The base class has pure virtual methods (= 0) andvirtual ~IInterface() = default;. - Generalization / Inherits (βββ·): Declared with
class Child : public Parent. Used for shared baseline implementation. - Composition (ββββΆ): Expressed with
std::unique_ptr<Part>or a direct value memberPart part_. If the parent is destroyed, the part is destroyed automatically. - Aggregation (ββββΆ): Expressed with
std::shared_ptr<Part>or a non-owning pointerPart*. The collaborator is injected from outside and outlives the container. - Dependency (βββΆ): Expressed with method arguments
void execute(const Order& order)or local stack variables. The class does not store the object as a member.
// Composition (ββββΆ) vs Aggregation (ββββΆ) in C++
class Order {
std::vector<OrderItem> items_; // Composition: Order owns items
};
class OrderController {
std::shared_ptr<NotificationService> notifier_; // Aggregation: shared collaborator
void place(const Order& order); // Dependency (βββΆ): transient parameter
};2.2Diagnostic Smell 1: The God Class (Violates SRP)
The God Class (or Bloated Class) knows too much and does too much. In food delivery, an OrderService that validates coupons, calculates tax, writes to the SQL database, charges credit cards, and formats push notifications is a classic God class. Any ticket touches this single file, triggering merge conflicts and regression bugs. The fix: decompose by concern into PriceCalculator, PaymentGateway, OrderRepository, and NotificationBus.
// SMELL: 5 responsibilities packed into one class
class OrderGod {
void validateCart();
void calculateTaxAndPackaging();
void executePaymentGateway();
void writeToPostgres();
void sendPushNotification();
};2.3Diagnostic Smell 2: Switch on Type Codes (Violates OCP)
When you see switch (paymentMethod) or if (type == "UPI") ... else if (type == "CARD"), the design is screaming for the Strategy Pattern. Every time product adds 'Crypto' or 'BNPL', you must modify the switch statement, re-test existing payment methods, and risk breaking production. The fix: replace conditionals with polymorphism behind a shared interface.
// SMELL: Violates OCP - every new type modifies this function
void pay(PaymentType type, int amount) {
if (type == PaymentType::UPI) { /* ... */ }
else if (type == PaymentType::CARD) { /* ... */ }
else if (type == PaymentType::WALLET) { /* ... */ }
}2.4Diagnostic Smell 3: Refused Bequest (Violates LSP)
When a derived class inherits from a base but cannot support its operations, it leaves the method empty or throws an exception (e.g. CashOnDeliveryPayment::refund() throwing std::logic_error("Cannot refund cash online")). The caller expects any PaymentMethod to be refundable, but substitution breaks at runtime. The fix: segregate the hierarchy into IRefundablePayment and INonRefundablePayment, or favor composition.
// SMELL: Violates LSP - subclass rejects base class contract
class CashOnDelivery : public PaymentMethod {
void refund(int amount) override {
throw std::logic_error("COD cannot be refunded online!");
}
};2.5Diagnostic Smell 4: Fat Interfaces & Shotgun Surgery
Fat Interfaces (violating ISP) force clients to implement dummy stubs for methods they do not need. Keep interfaces small, cohesive, and role-focused. Shotgun Surgery occurs when a single requirement change (like updating tax calculation) requires modifying 10 separate files. Shotgun surgery indicates low cohesion and tight coupling; the solution is consolidating the scattered logic behind a single dedicated component.
// SMELL: Fat Interface forcing dummy stubs
class IWorker {
virtual void writeCode() = 0;
virtual void testSoftware() = 0;
virtual void designUi() = 0;
};UML class structure
All 5 UML relationships in one diagram: OrderController inherits BaseController (βββ·), implements/aggregates NotificationService (ββββΆ), composes OrderItems in Order (ββββΆ), and depends on Order as a parameter (βββΆ). EmailNotification implements NotificationService (βββ·).
- + ~BaseController() = default
- + logAction(action: string_view): void
- - notifier_: shared_ptr<NotificationService>
- + placeOrder(order: const Order&, email: string_view): bool
- + ~NotificationService() = default
- + send(recipient: string_view, msg: string_view): void = 0
- - sentCount_: int
- + send(recipient: string_view, msg: string_view): void override
- + sentCount(): int
- - id_: string
- - items_: vector<OrderItem>
- + addItem(sku: string, price: int, qty: int): void
- + calculateTotal(): int
- + sku: string
- + priceRupees: int
- + quantity: int
Runnable C++23 example
- 1.
Generalization (Inherits βββ·):
OrderControllerinherits fromBaseController. It callslogActionto record operations without reimplementing audit logging. - 2.
Realization (Implements βββ·):
EmailNotificationimplements the pure virtual interfaceNotificationService, fulfilling the contract with anoverridesendmethod. - 3.
Composition (Composes ββββΆ):
Ordermaintains astd::vector<OrderItem>value member. When theOrderobject is destroyed, all itsOrderIteminstances are automatically reclaimed from memory. - 4.
Aggregation (Aggregates ββββΆ):
OrderControlleraggregates astd::shared_ptr<NotificationService>. The notification service is created independently outside the controller and continues living even if the controller is discarded. - 5.
Dependency (Depends βββΆ):
OrderController::placeOrderacceptsconst Order&as a transient method parameter, performing calculations without retaining a permanent member pointer.
#include <iostream>
#include <memory>
#include <numeric>
#include <print>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
// 1. Inherits (βββ·): BaseController provides shared logging and audit utilities
class BaseController {
public:
virtual ~BaseController() = default;
void logAction(std::string_view action) const {
std::println("[BASE-AUDIT] Executed: {}", action);
}
};
// 2. Implements (βββ·): NotificationService is a pure virtual interface contract
class NotificationService {
public:
virtual ~NotificationService() = default;
virtual void send(std::string_view recipient, std::string_view message) = 0;
};
class EmailNotification final : public NotificationService {
int sentCount_{0};
public:
void send(std::string_view recipient, std::string_view message) override {
sentCount_++;
std::println("[EMAIL] To {}: {}", recipient, message);
}
[[nodiscard]] int sentCount() const noexcept {
return sentCount_;
}
};
// 3. Composes (ββββΆ): Order strictly owns its OrderItems (value composition)
struct OrderItem {
std::string sku;
int priceRupees;
int quantity;
};
class Order {
std::string id_;
std::vector<OrderItem> items_; // Composition: items cannot exist outside the order
public:
explicit Order(std::string id) : id_(std::move(id)) {}
void addItem(std::string sku, int priceRupees, int quantity) {
items_.push_back(OrderItem{std::move(sku), priceRupees, quantity});
}
[[nodiscard]] int calculateTotal() const noexcept {
return std::accumulate(items_.begin(), items_.end(), 0,
[](int sum, const OrderItem& it) {
return sum + (it.priceRupees * it.quantity);
});
}
[[nodiscard]] const std::string& id() const noexcept { return id_; }
[[nodiscard]] const std::vector<OrderItem>& items() const noexcept { return items_; }
};
// 4. Aggregates (ββββΆ) & Depends (βββΆ):
// - Aggregates NotificationService via std::shared_ptr (notifier outlives controller)
// - Inherits BaseController (βββ·)
// - Depends on Order as a method parameter (βββΆ)
class OrderController final : public BaseController {
std::shared_ptr<NotificationService> notifier_; // Aggregation: shared non-exclusive ownership
public:
explicit OrderController(std::shared_ptr<NotificationService> notifier)
: notifier_(std::move(notifier)) {}
// Depends (βββΆ): transient usage of Order without holding ownership
bool placeOrder(const Order& order, std::string_view customerEmail) {
logAction(std::format("placeOrder for {}", order.id()));
int total = order.calculateTotal();
std::println("[CONTROLLER] Order {} verified. Total: Rs.{}", order.id(), total);
if (notifier_) {
notifier_->send(customerEmail,
std::format("Order {} placed successfully for Rs.{}", order.id(), total));
}
return true;
}
};
int main() {
// Aggregation participant created independently (can outlive controller)
auto emailService = std::make_shared<EmailNotification>();
// Controller created, aggregating emailService and inheriting BaseController
OrderController controller(emailService);
// Composition: Order owns its OrderItem instances
Order order("ORD-8801");
order.addItem("SKU-BURGER", 250, 2);
order.addItem("SKU-FRIES", 120, 1);
// Dependency: Order passed into placeOrder by reference
controller.placeOrder(order, "naman@example.com");
std::println("Notifications sent: {}", emailService->sentCount());
return 0;
}[BASE-AUDIT] Executed: placeOrder for ORD-8801 [CONTROLLER] Order ORD-8801 verified. Total: Rs.620 [EMAIL] To naman@example.com: Order ORD-8801 placed successfully for Rs.620 Notifications sent: 1
Rules of thumb
Composition (β) over Aggregation (β) by default: If a component is solely managed by its parent, express it as a value member or
std::unique_ptr. Only use aggregation when the resource is shared or injected as an independent service.Limit interview diagrams to 3 to 6 boxes: An interview board crowded with 20 boxes obscures the design. Focus strictly on the primary interfaces, concrete strategies, and the caller context.
Spot the code smell and immediately name the SOLID fix: When an interviewer presents a scenario with a
switchon type codes, immediately state: 'This violates the Open-Closed Principle; we should refactor to a polymorphic Strategy interface.'Dashed arrows represent contracts and ephemeral dependencies: Dashed triangle (βββ·) signifies pure virtual interface implementation; dashed open arrow (βββΆ) signifies parameter usage or temporary creation.
Never inherit just to reuse a single helper method: If class
Bneeds one utility method from classA, do not inherit (βββ·). ComposeAor pass it as a dependency to avoid Refused Bequest (LSP violation).
Don't make this mistake
Composition means the part cannot exist without the whole. If the parent dies, the child dies. Aggregation represents a loose, shared relationship where the part exists independently.
Interviews test architectural boundaries and design patterns, not diagram clutter. Focus on the polymorphic relationships and ownership semantics.
Subclasses that stub out methods with exceptions violate LSP and cause runtime crashes when callers treat them as full base implementations. Split the interface using ISP.