2Brute2Best
πŸ”₯0
F3

Interfaces in C++

High-frequency interview topicProgram to an Interface = 0 = default
⚑ 60-Second Architecture SummaryF3 · Foundation
4 Core ConceptsC++23 Verified Demo4 Rules of Thumb
Why It Matters in High-Scale Systems

The golden rule of resilient software architecture is program to an interface, not an implementation. When a business service directly instantiates a concrete third-party SDK (like Twilio SMS or SendGrid Email), it binds tightly to external infrastructure. It cannot be unit-tested without network calls, cannot switch providers without editing business logic, and violates the Dependency Inversion Principle. An interface creates an architectural boundary that isolates business rules from operational details.

Core Mental Model

Think of an interface as a legal service contract signed by an external vendor. The contract specifies what work must be done (send notification) and the format of the outcome (bool success). The client company doesn't care whether the contractor delivers by motorcycle, electric van, or drone, as long as the terms of the contract are fulfilled.

01

Why this matters in LLD interviews

The golden rule of resilient software architecture is program to an interface, not an implementation. When a business service directly instantiates a concrete third-party SDK (like Twilio SMS or SendGrid Email), it binds tightly to external infrastructure. It cannot be unit-tested without network calls, cannot switch providers without editing business logic, and violates the Dependency Inversion Principle. An interface creates an architectural boundary that isolates business rules from operational details.

Mental model

Think of an interface as a legal service contract signed by an external vendor. The contract specifies what work must be done (send notification) and the format of the outcome (bool success). The client company doesn't care whether the contractor delivers by motorcycle, electric van, or drone, as long as the terms of the contract are fulfilled.

02

Core concepts

2.1Pure Virtual Classes (= 0) as C++ Interfaces

C++ has no dedicated interface keyword like Java or C#. Instead, an interface in C++ is an abstract class with only pure virtual methods, no state variables, and a virtual destructor.

The syntax = 0 tells the compiler that this class cannot be instantiated directly; any concrete subclass must override every pure virtual method to be instantiated. The declaration virtual ~Notifier() = default; ensures clean polymorphic destruction through base interface pointers.

C++23 snippet
class Notifier {
public:
    virtual ~Notifier() = default;
    virtual bool send(std::string_view recipient, std::string_view msg) = 0;
    virtual std::string channelName() const = 0;
};

2.2Programming to an Interface, Not an Implementation

When OrderService accepts a std::shared_ptr<Notifier> (or std::unique_ptr<Notifier>), it knows nothing about HTTP endpoints, API tokens, or telecom protocols. It simply invokes notifier_->send(...).

This decouples the caller completely. When product management asks to replace Twilio with Gupshup or add a WhatsApp gateway, the payments and ordering teams do not touch or re-test OrderService. They simply implement a new WhatsAppNotifier : public Notifier.

C++23 snippet
class OrderService {
    std::shared_ptr<Notifier> notifier_;
public:
    explicit OrderService(std::shared_ptr<Notifier> notifier)
        : notifier_(std::move(notifier)) {}
};

2.3Dependency Injection & Test Doubles (Fakes)

The greatest practical benefit of interfaces is testability. In unit tests, passing real SMS or email gateways causes slow tests, random network flakiness, and incurred API bills.

With interfaces, tests inject a FakeNotifier that records notifications in an in-memory std::vector. Tests execute in microseconds, run 100% deterministically in CI environments, and can easily simulate failure scenarios (e.g. gateway timeouts or invalid recipients).

C++23 snippet
class FakeNotifier final : public Notifier {
public:
    std::vector<std::string> sentMessages;
    bool send(std::string_view, std::string_view msg) override {
        sentMessages.push_back(std::string(msg));
        return true;
    }
};

2.4Interface Segregation: Small Contracts over Monoliths

Keep interfaces lean and focused. Avoid fat interfaces that bundle unrelated operations like sendNotification(), chargeCreditCard(), and generatePdfReceipt() into one mega-contract. If a client only needs notifications, define a dedicated Notifier interface. This honors the Interface Segregation Principle (ISP) and ensures mock implementations stay simple.

C++23 snippet
// Prefer small, focused interfaces:
class Notifier { /* send only */ };
class ReceiptPrinter { /* print only */ };
03

UML class structure

OrderService depends strictly on the Notifier interface. Concrete implementations and test fakes implement the pure virtual contract.

Β«interfaceΒ»
Notifier
Pure virtual contract (= 0)
  • + ~Notifier() = default
  • + send(recipient, message): bool = 0
  • + channelName(): string = 0
Used by:OrderService(β—‡)SmsNotifier(β–³)EmailNotifier(β–³)FakeNotifier(β–³)
SmsNotifier
Production SMS channel
β–³ implementsNotifier
  • - senderId_: string
  • + send(recipient, message): bool override
  • + channelName(): string override
EmailNotifier
Production Email channel
β–³ implementsNotifier
  • - fromAddress_: string
  • + send(recipient, message): bool override
  • + channelName(): string override
FakeNotifier
In-memory test double
β–³ implementsNotifier
  • + history: vector<SentItem>
  • + shouldFail: bool
  • + send(recipient, message): bool override
  • + channelName(): string override
OrderService
High-level domain service
β—‡ aggregatesNotifier
  • - notifier_: shared_ptr<Notifier>
  • + OrderService(notifier)
  • + setNotifier(notifier): void
  • + notifyOrderStatus(id, contact, status): bool
  • + currentChannel(): string
Relationships (UML arrows)
OrderService◇──▢Notifierdepends on contract
SmsNotifierβ•Œβ•Œβ–·Notifierimplements
EmailNotifierβ•Œβ•Œβ–·Notifierimplements
FakeNotifierβ•Œβ•Œβ–·Notifierimplements
04

Runnable C++23 example

What the demo shows
  1. 1.

    Interface Declaration: Notifier specifies the pure virtual contract send() and channelName() with zero member variables and a virtual destructor.

  2. 2.

    Concrete Implementations: SmsNotifier and EmailNotifier implement the contract according to channel-specific formatting and validation rules.

  3. 3.

    Caller Decoupling: OrderService holds a pointer to Notifier. It constructs and delivers status updates without knowing whether SMS or Email is active.

  4. 4.

    Runtime Adaptability: Calling setNotifier() swaps the active notification channel dynamically while keeping all service logic unchanged.

Foundation Demo Β· demo.cpp
#include <format>
#include <memory>
#include <print>
#include <string>
#include <string_view>
#include <vector>

// Pure virtual interface representing a notification channel contract.
// Interfaces in C++ have no member variables and declare pure virtual (= 0) methods.
class Notifier {
public:
    virtual ~Notifier() = default;

    [[nodiscard]] virtual bool send(std::string_view recipient, std::string_view message) = 0;
    [[nodiscard]] virtual std::string channelName() const = 0;
};

// Production SMS notification channel.
class SmsNotifier final : public Notifier {
private:
    std::string senderId_;

public:
    explicit SmsNotifier(std::string senderId) : senderId_(std::move(senderId)) {}

    [[nodiscard]] bool send(std::string_view recipient, std::string_view message) override {
        if (recipient.empty() || message.empty()) {
            return false;
        }
        std::println("[SMS:{}] Sending to {}: \"{}\"", senderId_, recipient, message);
        return true;
    }

    [[nodiscard]] std::string channelName() const override {
        return "SMS";
    }
};

// Production Email notification channel.
class EmailNotifier final : public Notifier {
private:
    std::string fromAddress_;

public:
    explicit EmailNotifier(std::string fromAddress) : fromAddress_(std::move(fromAddress)) {}

    [[nodiscard]] bool send(std::string_view recipient, std::string_view message) override {
        if (recipient.find('@') == std::string_view::npos || message.empty()) {
            return false;
        }
        std::println("[Email:{}] Sending to <{}>: \"{}\"", fromAddress_, recipient, message);
        return true;
    }

    [[nodiscard]] std::string channelName() const override {
        return "Email";
    }
};

// High-level business service: depends strictly on Notifier interface, never on concrete SMS or Email.
class OrderService {
private:
    std::shared_ptr<Notifier> notifier_;

public:
    explicit OrderService(std::shared_ptr<Notifier> notifier)
        : notifier_(std::move(notifier)) {}

    void setNotifier(std::shared_ptr<Notifier> notifier) noexcept {
        notifier_ = std::move(notifier);
    }

    [[nodiscard]] bool notifyOrderStatus(std::string_view orderId,
                                         std::string_view customerContact,
                                         std::string_view status) {
        std::string message = std::format("Order {}: status is now '{}'", orderId, status);
        return notifier_->send(customerContact, message);
    }

    [[nodiscard]] std::string currentChannel() const {
        return notifier_->channelName();
    }
};

int main() {
    auto sms = std::make_shared<SmsNotifier>("FOODAPP");
    OrderService orderService(sms);

    std::println("Using channel: {}", orderService.currentChannel());
    (void)orderService.notifyOrderStatus("ORD-7001", "+919876543210", "Confirmed by Restaurant");

    // Seamlessly swap to Email at runtime without touching OrderService
    auto email = std::make_shared<EmailNotifier>("support@foodapp.com");
    orderService.setNotifier(email);

    std::println("Swapped channel: {}", orderService.currentChannel());
    (void)orderService.notifyOrderStatus("ORD-7001", "customer@example.com", "Out for Delivery");

    return 0;
}
Program output (stdout)
Using channel: SMS
[SMS:FOODAPP] Sending to +919876543210: "Order ORD-7001: status is now 'Confirmed by Restaurant'"
Swapped channel: Email
[Email:support@foodapp.com] Sending to <customer@example.com>: "Order ORD-7001: status is now 'Out for Delivery'"
05

Modern C++ note

C++ Pure Virtual Interfaces vs C++20 ConceptsC++20
What it replaces

Runtime virtual tables when concrete types are known at compile time.

Why it is better

With C++20 concepts, you can define interface requirements at compile time without any virtual functions, vtable indirection, or heap allocations. The compiler inlines calls directly while still verifying function signatures.

When classic virtual wins

When the concrete provider must be swapped dynamically at runtime (e.g. from user configuration or fallback routers), or when different implementations must be stored in the same heterogeneous collection (vector<unique_ptr<Notifier>>).

06

Rules of thumb

  • Never put data members inside an interface classβ€”keep it 100% pure virtual.

  • Always provide a public virtual ~Interface() = default; in interface definitions.

  • Inject dependencies as interface pointers or references into constructors rather than creating concrete instances internally.

  • Design interfaces from the client's perspective: declare what the caller needs, not what the provider happens to implement.

07

Don't make this mistake

Adding member variables to an interface
βœ— class Notifier { public: std::string apiKey_; // State in interface! virtual bool send(...) = 0; };
βœ“ class Notifier { public: virtual ~Notifier() = default; virtual bool send(...) = 0; };

Adding state to interfaces forces unnecessary memory layout and initialization coupling onto all derived classes. State belongs in concrete subclasses.

Directly instantiating concrete dependencies inside services
βœ— class OrderService { SmsNotifier sms_; public: OrderService() : sms_("KEY") {} };
βœ“ class OrderService { std::shared_ptr<Notifier> notifier_; public: explicit OrderService(std::shared_ptr<Notifier> n) : notifier_(std::move(n)) {} };

Constructing concrete classes inside services tightly couples them to real infrastructure, preventing unit testing and runtime channel swapping.

Fat monolithic interfaces violating ISP
βœ— class ServiceContract { virtual void sendNotification() = 0; virtual void processPayment() = 0; virtual void updateInventory() = 0; };
βœ“ class Notifier { virtual void sendNotification() = 0; }; class PaymentGateway { virtual void processPayment() = 0; };

Monolithic interfaces force classes to implement stub or throw-away methods for operations they do not support, complicating unit tests and maintenance.

08

Quick interview check

09

Mark your progress

Systems Integration

Related C++ Systems & Algorithmic Foundations

Deep dive into underlying layers