What you'll learn
Architecture is the deliberate placement of responsibilities and dependencies so change stays local. Patterns are names for recurring tradeoffs, not goals by themselves. You will design boundaries around values, ownership, errors, polymorphism, compilation, and compatibility.
By the end, you'll be able to:
- Evaluate cohesion, coupling, dependency direction, and stable boundaries
- Choose value, object, generic, and runtime-polymorphic designs deliberately
- Apply selected patterns without hiding ownership or control flow
- Publish APIs with explicit lifetime, failure, concurrency, and compatibility contracts
Cohesion and dependency direction
Cohesion keeps related data and behavior together. Coupling measures how many details one component must know about another. A stable boundary presents domain concepts while hiding volatile technology details. Source dependencies should point toward stable policy, with file systems, networks, and frameworks adapted at the edges.
CLI / GUI / HTTP adapter
|
v
Application use cases ---> abstract ports (Clock, Store, Logger)
| ^
v | implements
Domain values and rules filesystem / database / network adaptersKey idea
Value, object, and generic design
Value-oriented design models independent data with equality and copy/move semantics. Object-oriented design protects state behind behavior and can vary implementation at runtime. Generic design expresses algorithms over compile-time capabilities. A system can use all three, selecting the simplest mechanism that preserves the contract.
| Style | Strong fit | Primary cost |
|---|---|---|
| Value | Domain facts, messages, results, configuration | Copying or large representation |
| Encapsulated object | Stateful invariant and cohesive behavior | Identity and mutation reasoning |
| Virtual interface | Open runtime implementations | Indirection, ownership, ABI |
| Template/concept | Open compile-time operations | Build cost and exposed definitions |
| Variant | Closed alternatives with value semantics | Visitation changes when the set grows |
SOLID in Modern C++
SOLID principles are prompts for evaluating change, not laws requiring inheritance. A class should have one cohesive reason to change; substitutable implementations must honor their base contract; focused interfaces reduce forced dependencies; high-level policy should not depend directly on volatile detail.
- Single responsibility: group one cohesive policy, not necessarily one method
- Open/closed: add behavior through data, composition, variants, templates, or interfaces as appropriate
- Liskov substitution: derived implementations preserve preconditions, postconditions, and invariants
- Interface segregation: consumers depend only on operations they need
- Dependency inversion: policy owns an abstraction that external detail implements
Watch out
Patterns as tradeoffs
Strategy supplies interchangeable behavior, factory centralizes validated construction, observer publishes events, adapter translates an external interface, command packages an action, and state delegates behavior by lifecycle phase. Each pattern adds objects and indirection; begin with direct code and introduce the pattern when the variation becomes real.
| Pattern | Variation it contains |
|---|---|
| Strategy | How an operation is performed |
| Factory | Which valid concrete value is constructed |
| Observer | Which subscribers react to an event |
| Adapter | How an external contract maps to an internal one |
| Command | Which action is stored, queued, logged, or undone |
| State | Which behavior applies in the current lifecycle state |
#include <functional>
#include <iostream>
#include <vector>
using Pricing = std::function<double(double)>;
class Checkout {
public:
explicit Checkout(Pricing pricing) : pricing_{std::move(pricing)} {}
double total(const std::vector<double>& prices) const {
double result{};
for (double price : prices) result += pricing_(price);
return result;
}
private:
Pricing pricing_;
};
int main() {
Checkout sale{[](double price) { return price * 0.9; }};
std::cout << sale.total({10.0, 20.0}) << '\n';
}Injection, Pimpl, and type erasure
Constructor injection makes required collaborators visible and testable. Pimpl stores a pointer to a hidden implementation, reducing header dependencies and helping ABI stability at the cost of allocation and indirection. Type erasure stores heterogeneous implementations behind one value-like runtime interface without requiring them to inherit a shared base.
#pragma once
#include <memory>
#include <string>
class Client {
public:
explicit Client(std::string endpoint);
~Client();
Client(Client&&) noexcept;
Client& operator=(Client&&) noexcept;
Client(const Client&) = delete;
Client& operator=(const Client&) = delete;
std::string fetch();
private:
class Impl;
std::unique_ptr<Impl> impl_;
};Note
API contracts and compatibility
A C++ API must communicate value ranges, ownership transfer, borrowed lifetimes, invalidation, thread safety, blocking, error channels, complexity, and version compatibility. Prefer domain types and spans to parallel raw parameters, return values to output mutation, and named operations to Boolean flag combinations.
// Weak: unclear ownership, units, bounds, and error signaling.
int send(void* data, int length, int timeout, bool async);
// Stronger: types state borrowing, byte count, duration, and typed failure.
#include <chrono>
#include <expected>
#include <span>
enum class SendError { disconnected, timed_out, rejected };
std::expected<std::size_t, SendError>
send(std::span<const std::byte> payload, std::chrono::milliseconds timeout);- Add new behavior compatibly before removing old behavior
- Deprecate with migration guidance and a documented timeline
- Keep ownership and error policy consistent across one API family
- Use characterization tests before refactoring legacy behavior
- Move unsafe mechanisms behind small adapters, then improve inward
Tip
Recap & quick check
Key takeaways
- Architecture keeps volatile details behind stable domain-oriented boundaries.
- Values, objects, templates, variants, and virtual interfaces solve different variation problems.
- SOLID principles guide contract and dependency review; they do not mandate class hierarchies.
- Patterns should name real variation and justify their added indirection.
- C++ APIs must state ownership, lifetime, errors, invalidation, concurrency, complexity, and compatibility.
Quick check
1. What should source dependencies generally point toward?
2. Which technique models a closed set of alternatives?
3. When is Pimpl justified most clearly?
4. What should a borrowed-view API document?
Next: Module 29 — Performance, Portability & Secure C++, where design claims are tested against measurements, platforms, and hostile inputs.