DEV Community

Cover image for Living architecture, enforced
Egor Kraev
Egor Kraev

Posted on

Living architecture, enforced

In my previous post, I described the two components of what makes code “good” for me from a high-level structure perspective, namely modularity and making sure it realizes the high-level intent of the codebase.

Here is how I make those constraints bite: the architecture folder of the SLayer repo contains a LikeC4 model of the major components of the codebase, and the dependencies between them. This is the binding high-level map of the system components. Here is how it is enforced:

architecture/index.yaml maps the names of the LikeC4 components to Python packages in the codebase. Eventually the goal is for those to align, but for now some of the LikeC4 components are ‘virtual’, mapped to a set of several modules.

Enforcement is one law, in tools/arch_check.py: the LikeC4 relations must exactly match the AST-measured import edges, at every granularity the model declares — where a component spells out its child modules, the arrows are checked child-by-child too. A declared arrow is the only thing that licenses an import; silence is a ban. So the code can’t drift from the declared granularity model, as a hard deterministic constraint. Grandfathered crossings live right in the model as dashed #legacy arrows, and arch_check pins their count to a baseline in architecture/index.yaml that may only ever be lowered — a PR can shrink that list but never quietly grow it.

To begin with, the LikeC4 model is quite coarse, and is being refined step by step as I touch the relevant areas of the codebase. The idea is not to have a fully granular, eternal grand plan, but rather to build up gradually the set of top-down architectural constraints to complement the bottom-up constraints provided by unit and integration tests, to make sure the codebase stays within the high-level design shape.

This takes care of the modularity structure, but the structure itself doesn’t say what each module is for, what principles and constraints it must observe. That’s the second half of the living architecture docs, contained in the .arc42.md files. Most of these files correspond to a node in the LikeC4 model, and describe the principles that this module has to follow. The relevant part of the LikeC4 model is auto-compiled into a Mermaid diagram (with dashed edges indicating deprecated dependencies) and included in the respective .arc42.md - see for example the one for the sql module.

Both the LikeC4 and the arc42 files are treated by all the skills in the same way as tests, that is any change to them must be specifically and explicitly be approved by me.

Importantly, each principle must come with a tag that denotes its status, either citing which tests enforce it, or that it’s believed to hold pending review, or that it still needs enforcing (citing relevant issue IDs). That way, the bigger vision can be represented coherently in one place, even if not all of it may be currently realized.

This highlights an important and natural division of labour between the OpenSpec files and these - OpenSpec files refer to specific features and functionalities, and are descriptive rather than normative. They describe what is, or in the case of change specs, what we’re about to do. On the other hand, these architectural docs map to actual code structure and are normative, they describe what should hold, on a sometimes fairly abstract principle level.

As the OpenSpec artefacts relate to specific features, some of them will cut across many architectural components. The link is formal: architecture/index.yaml maps every OpenSpec spec group either to its owning node or to a cross_cutting_specs entry listing the nodes it touches, and arch_check fails on any spec group that is unmapped, mapped but missing on disk, or empty.

How are the .arc42.md used? During the planning stage, the agent reads the architecture docs for the relevant modules so it can make sure any changes it makes align with them - and also as it addresses review comments. During the test-writing phase, the agent can check which of the principles are relevant for the code being planned, and make sure its adherence to the relevant principles are covered by tests.

And on the other hand, they give me a clear place to express and review the high-level laws that hold for a given component - that, for me, is a large part of what code quality is about.

I believe this combination of modularity enforced via LikeC4, and principle-based planning and review for each module, allows me to achieve a degree of high-level coherence and architectural cleanliness superior to that that my hand-written code ever had back in the day (and will keep improving gradually, as I refine it) - just like my PR workflow gives me better corner case handling and test coverage than I ever achieved on my own.

If you believe this combination still misses some important facet of what good code is about, please tell me what it is - I’d love to learn!

And do let me know if you'd like me to refactor that into a reusable collection of skills + scripts.

Top comments (0)