Modern C++ architecture
Notes on structuring large C++ codebases without drowning in headers, build times, and accidental coupling.
Most C++ projects don't collapse because of a clever bug. They collapse under their own weight - a header that includes a header that includes everything, a build that takes four minutes to tell you about a typo, a "utils" namespace that quietly became the whole program.
Headers are an interface, not a dumping ground
Treat a header as a contract. If a type only ever appears behind a pointer or reference, forward-declare it and keep the include out of the header entirely. The compiler stops re-parsing transitive definitions, and your incremental builds get noticeably faster. The discipline also forces you to think about what a module actually exposes versus what it merely uses.
// renderer.hpp
class Device; // forward declaration is enough here
class Renderer {
public:
explicit Renderer(Device& device);
void draw(const Mesh& mesh);
private:
Device& device_;
};Value semantics first, pointers when you must
Modern C++ rewards designing around values. std::vector, std::string, and std::optional make ownership obvious and copies cheap to reason about. Reach for unique_ptr when you need polymorphism or stable addresses, and shared_ptr only when ownership is genuinely shared - which is rarer than most codebases assume.
Dependency direction is the whole game
Draw the arrows. If your rendering layer knows about your asset loader and your asset loader knows about your rendering layer, you don't have two modules - you have one module pretending to be two. Push the shared abstractions down into a layer both can depend on, and keep the arrows pointing one way.
The payoff isn't architectural purity for its own sake. It's that you can open a file, understand it in isolation, change it, and rebuild in seconds instead of minutes.