2Brute2Best
πŸ”₯0
F6

Reading Class Diagrams & Code Smells

UMLSOLID Map
⚑ 60-Second Architecture SummaryF6 · Foundation
5 Core ConceptsC++23 Verified Demo5 Rules of Thumb
Why It Matters in High-Scale Systems

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.

Core Mental Model

The Two Pillars of Structural Design:

  1. 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 member Part part_). The part's lifetime is strictly bounded by the whole.
    • Aggregates (◇──▢): Shared or borrowed association (std::shared_ptr<Part> or Part*). 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.
  2. 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.
01

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.

Mental model

The Two Pillars of Structural Design:

  1. 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 member Part part_). The part's lifetime is strictly bounded by the whole.
    • Aggregates (◇──▢): Shared or borrowed association (std::shared_ptr<Part> or Part*). 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.
  2. 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.
02

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) and virtual ~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 member Part part_. If the parent is destroyed, the part is destroyed automatically.
  • Aggregation (◇──▢): Expressed with std::shared_ptr<Part> or a non-owning pointer Part*. 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.
C++23 snippet
// 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.

C++23 snippet
// 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.

C++23 snippet
// 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.

C++23 snippet
// 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.

C++23 snippet
// SMELL: Fat Interface forcing dummy stubs
class IWorker {
    virtual void writeCode() = 0;
    virtual void testSoftware() = 0;
    virtual void designUi() = 0;
};
03

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 (β•Œβ•Œβ–·).

Β«abstractΒ»
BaseController
Inherited by controllers
  • + ~BaseController() = default
  • + logAction(action: string_view): void
Used by:OrderController(β–²)
OrderController
Context aggregating notifier & depending on order
▲ inheritsBaseController◇ aggregatesNotificationService⇒ depends onOrder
  • - notifier_: shared_ptr<NotificationService>
  • + placeOrder(order: const Order&, email: string_view): bool
Β«interfaceΒ»
NotificationService
Abstract contract
  • + ~NotificationService() = default
  • + send(recipient: string_view, msg: string_view): void = 0
Used by:EmailNotification(β–³)OrderController(β—‡)
EmailNotification
β–³ implementsNotificationService
  • - sentCount_: int
  • + send(recipient: string_view, msg: string_view): void override
  • + sentCount(): int
Order
Exclusively owns items
β—† composesOrderItem
  • - id_: string
  • - items_: vector<OrderItem>
  • + addItem(sku: string, price: int, qty: int): void
  • + calculateTotal(): int
Used by:OrderController(β‡’)
Β«structΒ»
OrderItem
  • + sku: string
  • + priceRupees: int
  • + quantity: int
Used by:Order(β—†)
Relationships (UML arrows)
OrderController──▷BaseControllerinherits
EmailNotificationβ•Œβ•Œβ–·NotificationServiceimplements
Order◆──▢OrderItemvector owns items
OrderController◇──▢NotificationServiceshares shared_ptr
OrderControllerβ•Œβ•Œβ–ΆOrdertakes as parameter
04

Runnable C++23 example

What the demo shows
  1. 1.

    Generalization (Inherits ──▷): OrderController inherits from BaseController. It calls logAction to record operations without reimplementing audit logging.

  2. 2.

    Realization (Implements β•Œβ•Œβ–·): EmailNotification implements the pure virtual interface NotificationService, fulfilling the contract with an override send method.

  3. 3.

    Composition (Composes ◆──▢): Order maintains a std::vector<OrderItem> value member. When the Order object is destroyed, all its OrderItem instances are automatically reclaimed from memory.

  4. 4.

    Aggregation (Aggregates ◇──▢): OrderController aggregates a std::shared_ptr<NotificationService>. The notification service is created independently outside the controller and continues living even if the controller is discarded.

  5. 5.

    Dependency (Depends β•Œβ•Œβ–Ά): OrderController::placeOrder accepts const Order& as a transient method parameter, performing calculations without retaining a permanent member pointer.

Foundation Demo Β· demo.cpp
#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;
}
Program output (stdout)
[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
06

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 switch on 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 B needs one utility method from class A, do not inherit (──▷). Compose A or pass it as a dependency to avoid Refused Bequest (LSP violation).

07

Don't make this mistake

Confusing Composition and Aggregation in C++ designs
βœ— // Storing unique_ptr but labeling it 'aggregation' in diagrams: class Engine {}; class Car { std::unique_ptr<Engine> engine_; // This is COMPOSTION (β—†), not aggregation! };
βœ“ // Composition: Car strictly owns Engine lifetime class Car { std::unique_ptr<Engine> engine_; // β—† Composes }; // Aggregation: Car borrows shared driver class Car { std::shared_ptr<Driver> driver_; // β—‡ Aggregates };

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.

Drawing every getter, setter, and utility class in UML
βœ— Drawing 30 boxes in an interview including 'StringHelper', 'IntWrapper', and every getter/setter.
βœ“ Draw 4-6 key boxes highlighting domain boundaries, contracts, and ownership arrows.

Interviews test architectural boundaries and design patterns, not diagram clutter. Focus on the polymorphic relationships and ownership semantics.

Leaving Refused Bequest unhandled in polymorphic hierarchies
βœ— class ReadOnlyRepository : public IRepository { void save(Entity e) override { /* empty or throw */ } };
βœ“ class IReadOnlyRepository { virtual Entity findById(int id) = 0; }; class IWriteRepository { virtual void save(Entity e) = 0; };

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.

08

Quick interview check

09

Mark your progress

Systems Integration

Related C++ Systems & Algorithmic Foundations

Deep dive into underlying layers