2Brute2Best
🔥0
F1

Classes & Encapsulation

EncapsulationClass Invariants [[nodiscard]] explicit
⚡ 60-Second Architecture SummaryF1 · Foundation
4 Core ConceptsC++23 Verified Demo4 Rules of Thumb
Why It Matters in High-Scale Systems

In low-level system design, encapsulation is not about reflexively adding getters and setters. It is about protecting class invariants—conditions that must hold true through every moment of an object's lifetime. If a Wallet balance is a public integer, any buggy caller or rogue discount logic can corrupt state to -500 rupees. Encapsulation establishes an unbreachable perimeter: internal state can only mutate via validated methods that enforce business rules.

Core Mental Model

Think of a struct as an open cashier tray and a class as a bank teller behind bulletproof glass. In an open tray, anyone can grab or drop coins without validation. At a teller counter, every deposit or withdrawal requires an identity check, updates balance limits, and strictly guarantees the vault balance never drops below zero.

01

Why this matters in LLD interviews

In low-level system design, encapsulation is not about reflexively adding getters and setters. It is about protecting class invariants—conditions that must hold true through every moment of an object's lifetime. If a Wallet balance is a public integer, any buggy caller or rogue discount logic can corrupt state to -500 rupees. Encapsulation establishes an unbreachable perimeter: internal state can only mutate via validated methods that enforce business rules.

Mental model

Think of a struct as an open cashier tray and a class as a bank teller behind bulletproof glass. In an open tray, anyone can grab or drop coins without validation. At a teller counter, every deposit or withdrawal requires an identity check, updates balance limits, and strictly guarantees the vault balance never drops below zero.

02

Core concepts

2.1class vs struct in C++: What Is the Real Difference?

In standard C++, a class and a struct differ in exactly one technical rule: default member access and default inheritance. In a struct, members and base classes are public by default; in a class, they are private by default.

In professional C++ design, this distinction carries strong idiomatic meaning: use a struct for passive data transfer objects (DTOs) where all fields are independent and any combination of values is valid (e.g. struct Money { int64_t paise; };). Use a class whenever private state and invariant logic must be protected.

C++23 snippet
struct Money { int64_t paise{0}; }; // Plain data bundle
class Wallet { int64_t balancePaise_{0}; /* private invariant */ };

2.2Class Invariants & Defensive Design

An invariant is a business rule that must never be violated. For a Wallet, invariants include:

  1. The balance can never be negative (balancePaise_ >= 0).
  2. The daily spending limit must be strictly positive (dailyLimitPaise_ > 0).
  3. The amount spent today cannot exceed the daily limit.

Constructors establish invariants; mutator functions preserve them. If a requested operation would break an invariant, the mutator rejects it early or throws, leaving the object in a clean, self-consistent state.

C++23 snippet
bool withdraw(int64_t amountPaise) {
    if (amountPaise <= 0 || amountPaise > balancePaise_) return false;
    if (spentTodayPaise_ + amountPaise > dailyLimitPaise_) return false;
    balancePaise_ -= amountPaise;
    spentTodayPaise_ += amountPaise;
    return true;
}

2.3const Correctness as an API Contract

Marking a member function const informs both the compiler and the caller that the method guarantees zero mutation of the object. In large systems, functions frequently accept parameters by const& (e.g. void renderBalance(const Wallet& w)). If your inspection methods lack const, the compiler refuses to compile the call, forcing callers into awkward workarounds or unsafe mutable references.

C++23 snippet
[[nodiscard]] int64_t balancePaise() const noexcept { return balancePaise_; }

2.4explicit Single-Argument Constructors & [[nodiscard]]

By default, any C++ constructor that can be called with a single argument acts as an implicit type conversion operator. If Wallet(int64_t balance) is not marked explicit, passing an integer 500 to a function expecting Wallet silently constructs a temporary wallet behind your back. Always prefix single-arg constructors with explicit.

Similarly, annotate mutators returning booleans or error codes with [[nodiscard]]. This turns accidental ignored errors into compile-time warnings.

C++23 snippet
explicit Wallet(std::string walletId, int64_t initialDepositPaise = 0);
[[nodiscard]] bool withdraw(int64_t amountPaise);
03

UML class structure

Wallet encapsulates balance and daily spending invariants. Money is a public value struct.

«struct»
Money
Passive data bundle
  • + paise: int64_t
  • + toRupees(): double
Used by:Wallet(⇢)
Wallet
Guarantees balance >= 0 and spend limit invariants
⇢ depends onMoney
  • - walletId_: string
  • - balancePaise_: int64_t
  • - dailyLimitPaise_: int64_t
  • - spentTodayPaise_: int64_t
  • + explicit Wallet(walletId, initialDeposit, limit)
  • + deposit(amountPaise): bool
  • + withdraw(amountPaise): bool
  • + balancePaise(): int64_t const
  • + remainingDailyLimitPaise(): int64_t const
  • + resetDailySpending(): void
Relationships (UML arrows)
Wallet╌╌▶Moneyformats values with
04

Runnable C++23 example

What the demo shows
  1. 1.

    Construction & Invariant Setup: The Wallet constructor validates that the identifier is non-empty, initial deposit is non-negative, and daily limit is positive before assigning private state.

  2. 2.

    Validated Withdrawal: withdraw() checks both available balance and daily spending limit before deducting paise. If either rule fails, the wallet state remains completely untouched.

  3. 3.

    Const Queries: balancePaise() and remainingDailyLimitPaise() are marked const noexcept, enabling safe inspection without side-effects.

Foundation Demo · demo.cpp
#include <cstdint>
#include <format>
#include <print>
#include <stdexcept>
#include <string>
#include <string_view>

// Value struct: pure passive data bundle with no invariant logic.
struct Money {
    int64_t paise{0};

    [[nodiscard]] double toRupees() const noexcept {
        return static_cast<double>(paise) / 100.0;
    }
};

// Class: maintains strict invariants over private state.
// Invariants:
// 1. balancePaise_ >= 0 (wallet cannot be overdrawn).
// 2. dailyLimitPaise_ > 0 (spending limit must be positive).
// 3. spentTodayPaise_ >= 0 and spentTodayPaise_ <= dailyLimitPaise_.
class Wallet {
private:
    std::string walletId_;
    int64_t balancePaise_{0};
    int64_t dailyLimitPaise_{100'000}; // Default limit Rs.1,000 (100,000 paise)
    int64_t spentTodayPaise_{0};

public:
    // Single-argument constructor marked explicit to prevent silent implicit conversions.
    explicit Wallet(std::string walletId, int64_t initialDepositPaise = 0, int64_t dailyLimitPaise = 100'000)
        : walletId_(std::move(walletId)),
          balancePaise_(initialDepositPaise),
          dailyLimitPaise_(dailyLimitPaise) {
        if (walletId_.empty()) {
            throw std::invalid_argument("Wallet ID cannot be empty");
        }
        if (initialDepositPaise < 0) {
            throw std::invalid_argument("Initial deposit cannot be negative");
        }
        if (dailyLimitPaise <= 0) {
            throw std::invalid_argument("Daily limit must be strictly positive");
        }
    }

    // Mutator with [[nodiscard]] so callers cannot ignore rejected deposits.
    [[nodiscard]] bool deposit(int64_t amountPaise) {
        if (amountPaise <= 0) {
            return false;
        }
        balancePaise_ += amountPaise;
        return true;
    }

    // Mutator: enforces both balance invariant and daily spend limit invariant.
    [[nodiscard]] bool withdraw(int64_t amountPaise) {
        if (amountPaise <= 0) {
            return false;
        }
        if (amountPaise > balancePaise_) {
            return false; // Invariant: balance cannot drop below 0
        }
        if (spentTodayPaise_ + amountPaise > dailyLimitPaise_) {
            return false; // Invariant: cannot exceed daily spending limit
        }

        balancePaise_ -= amountPaise;
        spentTodayPaise_ += amountPaise;
        return true;
    }

    // Const inspection methods: guarantee zero mutation on const instances.
    [[nodiscard]] int64_t balancePaise() const noexcept {
        return balancePaise_;
    }

    [[nodiscard]] int64_t remainingDailyLimitPaise() const noexcept {
        return dailyLimitPaise_ - spentTodayPaise_;
    }

    [[nodiscard]] const std::string& walletId() const noexcept {
        return walletId_;
    }

    void resetDailySpending() noexcept {
        spentTodayPaise_ = 0;
    }
};

int main() {
    Wallet customerWallet("wal_usr_9021", 50'000, 30'000); // Rs.500 balance, Rs.300 daily limit
    std::println("Created wallet {} with balance Rs.{:.2f}",
                 customerWallet.walletId(),
                 Money{customerWallet.balancePaise()}.toRupees());

    bool w1 = customerWallet.withdraw(20'000); // Withdraw Rs.200
    std::println("Withdraw Rs.200 -> ok={}, remaining balance Rs.{:.2f}, daily limit left Rs.{:.2f}",
                 w1,
                 Money{customerWallet.balancePaise()}.toRupees(),
                 Money{customerWallet.remainingDailyLimitPaise()}.toRupees());

    bool w2 = customerWallet.withdraw(15'000); // Attempt Rs.150 -> exceeds remaining limit Rs.100
    std::println("Withdraw Rs.150 (exceeds limit) -> ok={}, balance Rs.{:.2f}",
                 w2,
                 Money{customerWallet.balancePaise()}.toRupees());

    bool d1 = customerWallet.deposit(10'000); // Deposit Rs.100
    std::println("Deposit Rs.100 -> ok={}, new balance Rs.{:.2f}",
                 d1,
                 Money{customerWallet.balancePaise()}.toRupees());

    return 0;
}
Program output (stdout)
Created wallet wal_usr_9021 with balance Rs.500.00
Withdraw Rs.200 -> ok=true, remaining balance Rs.300.00, daily limit left Rs.100.00
Withdraw Rs.150 (exceeds limit) -> ok=false, balance Rs.300.00
Deposit Rs.100 -> ok=true, new balance Rs.400.00
05

Modern C++ note

[[nodiscard]] with reason strings and explicit constructors in Modern C++C++20
What it replaces

Silent implicit conversions and ignored error return values.

Why it is better

The compiler stops bugs before code even runs. In C++20, [[nodiscard("ignoring transaction status risks data loss")]] provides human-readable diagnostics right in the IDE whenever a developer forgets to check whether an operation succeeded.

When classic virtual wins

Plain aggregate structs (like 2D/3D vectors or configuration bags) where any combination of values is valid and no invariant checks are needed.

06

Rules of thumb

  • Use a struct when all fields are independent and any state is valid; use a class whenever modifying one variable requires validating or updating another.

  • Mark every single-argument constructor explicit by default unless implicit conversion is the deliberate design intent.

  • Mark every member function that does not modify member variables as const and annotate critical state-querying or error-returning methods with [[nodiscard]].

  • Never return non-const references or raw pointers to private internal containers.

07

Don't make this mistake

Public state with external validation logic
✗ struct Wallet { int64_t balance; }; // Caller code: if (w.balance >= amount) w.balance -= amount;
✓ class Wallet { private: int64_t balance_; public: [[nodiscard]] bool withdraw(int64_t amount); };

Leaving fields public duplicates validation across every caller in the codebase. When business rules change (e.g. adding a daily cap), you must update 50 call sites instead of one private method.

Forgetting explicit on single-argument constructors
✗ class Wallet { public: Wallet(int64_t balance); }; void processPayment(Wallet w); processPayment(500); // Compiles! Silently creates a temporary wallet.
✓ class Wallet { public: explicit Wallet(int64_t balance); }; processPayment(500); // Compile error: no matching conversion.

Single-arg constructors without explicit act as implicit conversions, leading to subtle runtime bugs and unexpected temporary object creation.

Missing const on read-only methods
✗ int64_t balance() { return balance_; }
✓ int64_t balance() const noexcept { return balance_; }

Without const, any function accepting const Wallet& cannot call balance(), breaking idiomatic C++ pass-by-const-reference patterns.

08

Quick interview check

09

Mark your progress

Systems Integration

Related C++ Systems & Algorithmic Foundations

Deep dive into underlying layers