Create your own
Lesson illustration

Detecting Circular Dependencies and Bidirectional Knowledge Flows

Good to see you again. You have now identified what each major part of the engine is supposed to be responsible for and, in particular, which jobs have accumulated inside Scene. Before moving any code, the next risk is hidden coupling: systems that appear separate but have quietly learned too much about one another.

This lesson is a dependency audit. You will distinguish a true circular dependency from ordinary collaboration, detect bidirectional knowledge even where C++ headers do not visibly form a cycle, and record the evidence for each issue. The result is a small cycle register that will keep the refactor grounded in what your engine actually does.


A circular dependency is a loop in required knowledge

A module dependency exists when one part of the code must know something about another in order to compile, construct objects, call an API, store a type, or access its state. A simple dependency is normal: your renderer needs to know about renderable data, and your game code needs to know about engine services.

A circular dependency exists when that required knowledge comes back around. For example:

  1. Scene depends on Renderer because it calls rendering functions.
  2. Renderer depends on Scene because it asks the scene for actors, cameras, and render components.
  3. Neither can be understood, compiled, or meaningfully changed in isolation.
The diagram depicts `app` depending on `module1`, `module2`, and `module3`, while `module2` also depends back on `app`; together, `app` and `module2` form a circular dependency.

The loop can involve two systems, as in the diagram, but it can also be longer:

  1. Scene knows Renderer.
  2. Renderer knows AssetService.
  3. AssetService reaches back into Scene.

In either case, the systems form a strongly connected group: once you enter it, following dependencies can eventually bring you back to where you began.

For your audit, distinguish three related but different problems:

ProblemWhat it meansTypical C++ evidence
Header/include cycleHeaders directly or indirectly include each other.Scene.hpp includes Renderer.hpp, which includes Scene.hpp.
Module or subsystem cycleLogical subsystems require each other, even if forward declarations avoid a direct include cycle.Scene.cpp calls renderer code; Renderer.cpp queries or mutates scene state.
Bidirectional knowledgeTwo systems understand each other’s internal structure, state, or rules. A visible compilation cycle may not exist.Renderer reads scene internals; scene configures OpenGL details or iterates renderer-owned GPU objects.

The third case is especially important. C++ can make a cycle look solved with include guards, forward declarations, pointers, or references. Those techniques can reduce compilation coupling, but they do not automatically remove a design problem.

For example, this may compile cleanly:

  • Scene holds a pointer to Renderer.
  • Renderer holds a pointer to Scene.
  • Both headers use forward declarations.
  • Each .cpp file includes the other header.

The direct include cycle may be gone, but the runtime architecture still says that each system needs the other’s full identity and capabilities. That is the dependency you are trying to see.


Think in terms of hierarchy and deliberate wiring

A useful default is that dependencies should travel in one intended direction. Higher-level code can assemble lower-level services, while lower-level services should not reach upward to discover who is using them or what editor/game context they happen to be inside.

How to write more flexible game code

Watch How to write more flexible game code by The Shaggy Dev. It gives a practical hierarchy rule for game code and shows why assumptions about parent or neighboring systems reduce reuse.

Watch the hierarchy rule for the argument that dependencies should descend through a structure rather than reach upward or sideways. Then watch encapsulation examples, focusing on the distinction between requesting a capability through a narrow method and manipulating another system’s internal data.

For your engine, a provisional hierarchy might eventually look like this:

AreaMay know aboutShould not need to know about
Platform adaptersGLFW, windowing, device APIs, OpenGL integration detailsgameplay rules, editor panels, particular actors
Engine runtime servicesstable engine data types and narrow world-facing interfacesImGui workflow, player-specific movement rules
Game codeengine runtime interfaces and game-specific actor/component typeseditor implementation details, raw GLFW calls
Editorengine runtime data and editor-only stateshipped-game-specific behavior as a runtime dependency
Application compositionall concrete services it deliberately creates and wires togethernothing needs to depend on it in return

Treat this as a direction to test, not yet as a final package layout. You are not required to create separate libraries or folders today. The immediate task is to find the places where the current code contradicts a sensible direction.

The application entry point, an editor host, or a focused frame coordinator may legitimately know about several systems. Its job is integration: it creates objects, chooses the frame phases, and supplies the right inputs. This kind of code is often called composition or glue code.

The mistake is making every reusable subsystem act like composition code. If Renderer reaches into Scene, Scene configures Renderer internals, and components fetch both through global access, the wiring has been spread everywhere.


Usage, creation, and ownership are different dependency strengths

Not all dependency arrows in your existing map carry the same architectural weight. Martin Fowler’s distinction between usage dependencies and creation dependencies is particularly useful here.

Refactoring Module Dependencies

Read Martin Fowler’s discussion of module dependency refactoring. The examples use Java and JavaScript, but the distinction between using a service, constructing/configuring it, and supplying it from a composition point maps directly to a C++ engine.

In the section “Linker Substitution,” begin at “Dividing up the code into several modules is helpful” and read the discussion of usage and creation dependencies. Focus on why constructing a concrete collaborator is more intimate coupling than merely calling a supplied interface. Then read the “Dependency Injection” section, from its opening explanation through the consequences. Notice that the composition/configuration phase can know concrete types without forcing the reusable component to know how those types are found or built.

Translate those ideas into your engine as follows:

Dependency kindMeaning in the engineExample
UsageA system calls a narrow operation supplied by another system.A player controller requests a collision query.
Type dependencyA public API exposes another concrete type.Renderer.hpp takes Scene& in its public Render method.
Creation dependencyA system constructs/configures a concrete collaborator.Scene creates PhotonClient, configures its callbacks, and owns its connection setup.
Storage/access dependencyA system retains broad access to another system.Every component stores Scene& and uses it as a universal lookup object.
Knowledge dependencyA system assumes another system’s state layout or rules.Editor code assumes that the first scene camera is always the player camera.

Creation dependencies deserve special attention. If Scene constructs the renderer, initializes Photon, registers ImGui callbacks, and creates gameplay actors, then Scene has learned a great deal about concrete configuration. If those systems also call back into Scene for their normal work, a cycle is very likely.

This does not mean that construction is bad. Something must create concrete objects. Prefer to concentrate that knowledge near startup or in a focused composition object, rather than make Scene the permanent creator and universal access point for every engine service.


Audit the current engine in four passes

Work from the dependency map you created earlier. Do this at the logical subsystem level first: Scene, renderer, collision, input, editor, networking/Photon adapter, game/player logic, asset handling, and platform code. Later, you can trace each suspect relationship down to individual headers and methods.

Create a working file:

docs/architecture/dependency-cycle-register.md

Begin with an edge-evidence table. Use the direction “depends on” in words rather than relying on folder names or assumptions.

Consumer subsystemDepends onDependency kindConcrete evidenceWhy it needs this knowledge
SceneRendererusage / creation / storageexact field, constructor call, or method callstate the immediate reason
RendererScenepublic type / query / callbackexact API, include, or callback targetstate the immediate reason

Use these four passes.

1. Find compile-time and public-API dependencies

Search the headers first. A dependency in a .hpp file is usually more consequential than one confined to a .cpp file because it becomes part of the public shape of the type.

Record project-local includes plus:

  • concrete types used as member fields;
  • base classes;
  • parameter and return types;
  • inline methods that call into another subsystem;
  • templates that require a full definition;
  • friend declarations that expose internals.

Forward declarations are useful evidence too. They show that a class at least needs another type’s identity, even if it does not require the full definition in its header.

Do not count standard-library use as an architectural issue. Your target is meaningful engine coupling: game code knowing editor classes, collision knowing player rules, renderer knowing Scene internals, or platform code knowing world objects.

2. Find creation and configuration dependencies

Next, inspect constructors, initialization code, scene loading, and startup paths. Search for:

  • std::make_unique, std::make_shared, new, and factory calls;
  • callback registration;
  • Photon client setup;
  • ImGui and GLFW initialization;
  • camera-mode selection setup;
  • registration of colliders, renderables, or networked objects.

Record who constructs whom. A concrete creation relationship often tells you where configuration knowledge has accumulated.

For example, if Scene creates the renderer and the renderer later asks Scene for every piece of world data, write down both facts separately. The first is a creation dependency. The second is a usage or access dependency. Together they expose a tighter relationship than either fact alone.

3. Find broad runtime access and hidden knowledge

Search for methods such as:

  • GetScene()
  • GetEngine()
  • GetRenderer()
  • GetActiveCamera()
  • GetActorBy...()
  • GetComponent...()

These names are not automatically wrong. The red flag is a caller receiving a broad object and then navigating through several unrelated domains.

For example, a movement component that receives Scene& merely to find collision data is coupled more broadly than its job requires. A renderer that takes Scene& and searches actors, chooses a camera, accesses editor flags, and loads missing resources is not simply rendering; it is assuming the structure and policy of the world.

Also inspect callbacks and lambdas. A callback installed by Photon or an editor panel may capture a Scene*, an actor pointer, or this. That is a dependency even if no function signature mentions Scene.

4. Perform the reverse-dependency probe

For every substantial dependency you wrote down, look for the reverse path:

  1. Start with a line such as “Scene depends on Renderer.”
  2. Search whether Renderer depends directly on Scene.
  3. If not, inspect its dependencies for an indirect route back through editor, assets, networking, game code, or a global registry.
  4. Record the full loop once you can support every leg with code evidence.

Do not stop after direct pairs. A three-system loop is just as real as a two-system loop.


Recognize bidirectional knowledge in familiar engine relationships

The following cases are plausible places to inspect in your prototype. They are examples to guide the audit, not claims about your current implementation. Only add them to the register when you find the corresponding code.

Scene and renderer

A common first version of a renderer has a method conceptually like Render(Scene&). The renderer traverses actors, locates meshes and materials, retrieves the active camera, binds OpenGL state, and draws.

At the same time, Scene may directly call renderer methods, configure shader behavior, decide render ordering, or hold renderer-owned GPU objects. This creates mutual knowledge.

A healthier eventual shape is often:

  • A coordinator selects a camera and obtains a narrow render-world view.
  • The renderer consumes that view and owns the GPU-facing work.
  • The renderer does not need the whole Scene.
  • The scene/world does not need to understand shader binding and draw submission.

You do not need to build that interface now. In the register, name the current mutual knowledge precisely: “Renderer scans scene actor storage” is much more useful than “renderer is coupled.”

Scene and collision

Collision naturally needs world-wide knowledge: it must consider colliders belonging to multiple actors. That does not require it to know everything Scene does.

A dependency loop appears when:

  • Scene owns the collision update and directly performs collision traversal.
  • The collision code calls back into Scene to enumerate actors or alter arbitrary transforms.
  • Gameplay components bypass collision APIs and manipulate collision-world structures through Scene.

Record whether collision needs:

  • a read-only view of colliders;
  • a registration point;
  • a query request;
  • authority to apply movement results.

Those are different requirements. The next module will address who owns objects and how long these references remain valid; this lesson only establishes where reciprocal knowledge currently exists.

Editor state and runtime world

Your editor camera, ImGui panels, selection state, and play/edit-mode switching are especially likely to create an upward dependency.

It is reasonable for editor code to inspect the runtime world. It is risky when runtime Scene code includes editor headers, chooses editor panels, stores editor selection state, or decides editor-camera controls. That makes the shipped runtime depend on the development tool that surrounds it.

A concrete warning sign is a runtime method with branches such as “if editor mode, use this camera/UI behavior.” Record that as bidirectional knowledge even if you have not yet split it into an editor module.

Networking integration and scene state

Photon integration often becomes tangled when SDK callbacks directly search the scene, create actors, mutate components, and trigger rendering- or editor-specific behavior. Then Scene also calls Photon processing from its update loop and retains Photon-specific objects.

Separate the two facts in your map:

  • who translates Photon callbacks and messages;
  • who decides what game state those messages represent.

That distinction will help you later isolate an adapter from game-specific replication policy.


Do not mistake mechanical fixes for architectural fixes

When you find a cycle, resist immediately changing includes. First, identify which dependency is conceptually backward or unnecessarily broad.

Mechanical changeWhat it can helpWhat it does not prove
Add include guardsPrevents repeated textual inclusion.The two systems no longer need each other.
Replace an include with a forward declarationReduces header compilation coupling.Runtime calls, construction, and internal knowledge are now one-way.
Replace a reference with a pointerMay permit incomplete types in a header.The stored relationship is less coupled.
Use a global singleton or Scene::Get...()Can remove a constructor parameter.The dependency has disappeared; it may simply be hidden.
Use std::shared_ptr in both directionsCan make references easy to store.The design is safe; it can also create a lifetime cycle.

A forward declaration is often worthwhile, but it is a tool, not a design decision. If Renderer still needs Scene to select a camera, enumerate actors, inspect editor state, and find assets, merely moving the include to Renderer.cpp has not solved the underlying problem.

The useful question is: which side should provide a narrow capability, and which side should stop reaching across the boundary?

There are four common candidate directions to record, without implementing them yet:

  1. Narrow the dependency. Replace whole-world access with the smallest needed interface or data view.
  2. Move orchestration upward. Let a focused coordinator hold both systems and call them in sequence, rather than making either one manage the other.
  3. Return data rather than mutate a peer. A collision query can return a result; the caller can apply an appropriate game decision.
  4. Keep the systems together when they truly change together. If separation requires continual back-and-forth knowledge, they may be one coherent subsystem rather than two independent ones.

The last option is legitimate, but use it carefully. “Put everything back in Scene” is not consolidation; it is a return to the original mixed-responsibility problem.

Jonas Tyroller’s distinction between reusable systems and small, purpose-built glue is useful here.

Best Code Architectures For Indie Games

Watch Best Code Architectures For Indie Games by Jonas Tyroller for a game-development perspective on avoiding mutual references while still connecting systems deliberately.

Watch the coupling warning for the direct-reference problem. Continue with focused glue code, noting that composition code can know multiple systems without making those systems depend on each other. Finally, watch small connectors for the warning that glue code itself should remain focused rather than becoming another giant manager.


Produce a cycle register, not a refactor plan

Finish the lesson by adding a concise register beneath the evidence table. One row represents one verified cycle or one serious case of bidirectional knowledge.

Cycle or mutual knowledgeEvidence for each dependencyWhy this is harmfulCandidate direction to investigate laterImmediate action
Scene and rendererAdd exact methods, fields, headers, and callbacksrenderer knows world policy; scene knows GPU detailsprovide a render-world view; move frame orchestration outside SceneRecord only
Scene and editorAdd exact play/edit branches and editor referencesruntime and authoring state cannot evolve independentlyeditor depends on runtime-facing APIs, not the reverseRecord only
collision and gameplayAdd exact actor traversal and movement mutation pathsbroad world access obscures collision authoritynarrow collision queries/resultsRecord only
Photon adapter and worldAdd callback and update-loop evidenceSDK behavior leaks into world lifecycleadapter translates messages; game integration interprets themRecord only

For each row, include these details in plain language:

  • the exact class, file, method, field, include, callback, or factory call that proves each relationship;
  • whether the dependency is usage, public type exposure, creation, stored access, or hidden knowledge;
  • whether it is direct or indirect;
  • the smallest candidate boundary that might break the loop later;
  • any ownership or lifetime question that must wait for the next module.

Keep uncertainty visible. If you find CameraComponent and Actor referencing each other, do not guess whether the final solution is an owner pointer, an ID, a reference, or another structure. Write down the relationship and mark lifetime decisions as deferred.

A productive 40-minute session here is enough to:

  1. watch the two short resource segments and read the focused Fowler excerpt;
  2. inspect Scene, renderer, collision, editor, and Photon integration for reverse dependencies;
  3. add evidence rows for every substantial relationship;
  4. identify direct cycles and longer indirect loops;
  5. record broad-access sites such as components receiving unrestricted Scene&;
  6. avoid changing code until the ownership consequences are clear.

Key takeaways

A circular dependency is not merely two headers including each other. It is a loop of required knowledge between modules or systems. C++ techniques such as include guards, forward declarations, pointers, and global lookup can change the shape of compilation dependencies while leaving architectural coupling intact.

Your strongest warning signs are reciprocal construction, broad Scene access, systems reading or mutating each other’s internals, runtime code knowing editor behavior, and SDK callbacks reaching directly across world boundaries. Record each issue with concrete evidence and a candidate boundary, but do not refactor it yet.

Next, you will turn this audit into architectural constraints: the behaviors that must remain intact during the refactor, including play mode, the editor camera, multiplayer, and packaged builds.

Can't find a good explanation? Sign up and we'll make it for you

Sign up