← BLOG
DevStationJune 27, 2026

Designed for extensibility

architecturedddcqrsrpcai

At some point in DevStation I had to rename deploy to install and destroy to uninstall. It sounds like a name swap, but the change cut across contract, domain, events, persistence and UI. This is the kind of operation that tends to reveal whether a project’s structure helps or gets in the way: when boundaries are implicit, a rename like that turns into a hunt for scattered references, and one always escapes. Here it ended up as a sequence of predictable steps, and that outcome was not luck, it was the visible part of a bet made at the start of the project.

DevStation models clusters, nodes, VMs, images, sizes, vault, provisioning, stations and blueprints, each carrying its own rules, and the list of features that fit a project like this has no end. So the question that guided the design was not “how do we ship today’s feature” but “how do we make room for the next one without compromising what already exists”.

Hexagonal: the domain isolated from the world

Hexagonal architecture (ports & adapters, proposed by Alistair Cockburn in 2005) exists precisely to isolate business logic from infrastructure and let it be tested independently. In DevStation, Proxmox, the TUI (the interface that runs inside the terminal), the file system and OpenTofu live at the edge, and the core sees them only as ports. That separation makes it possible to validate the boundary with architecture tests on every commit: the domain imports no infrastructure, and the inbound side never imports the outbound. So far, adding a provider or swapping the interface has not touched business rules, which is exactly what I expected to buy with that separation.

The layout matches what Herberto Graça catalogued as Explicit Architecture, his compilation of hexagonal, onion and clean into a single map, drawn from what was already practiced: an App Core (the Domain wrapped by the Application layer) with ports as the boundary and every dependency pointing inward. The naming here is inbound (driving) and outbound (driven), in place of primary/secondary.

Tactical DDD + CQRS: small contexts that grow without friction

The resources are organized into isolated bounded contexts (cluster, vault, station, service, images, size, blueprint, auth), each with its own aggregates, commands and events. Reads and writes are separated following CQRS. As Martin Fowler describes the pattern, it is about using different models to read and to write, and it is worth remembering that CQRS requires neither a separate database nor event sourcing. Here it is single-store CQRS, where the queries project records directly, bypassing the aggregates.

Where this genuinely helped me was maintenance. Extracting images into its own context meant replaying a known pattern, not inventing one. Renaming definition to size across the whole stack was mechanical because the boundary was explicit. And the deploy → install rename that opened this article only stayed a disciplined operation because each context knows what belongs to it.

Internal events between contexts

DevStation is a single binary, not a distributed system. Even so, I used domain events to keep the contexts from depending directly on each other: a policy reacts to another context’s event and dispatches a command of its own. The project’s context map records this as a rule: cross-context communication is never direct. When a station finishes an install, the cluster context records the service projection on the VM and the vault stores the published secrets, both through policies.

In a single binary this adds indirection, and in a smaller system it would be overkill. I accepted the cost for a specific reason: I want to keep explicit a boundary I intend to preserve in case parts of the engine are split out in the future, into a shared service or something along those lines. Keeping that door open now is cheaper than reopening it later. If the split never happens, I will have paid indirection for insurance I never used, and I consider that a reasonable price.

JSON-RPC contracts: the interface is detachable

The engine exposes a JSON-RPC boundary over stdio, and the contracts (OpenRPC, with code generated from them) are the real edge. The migration to that boundary was deliberately incremental: one context first, with no dual path, and a formal review before expanding to the rest, because mistakes in envelope and contract modeling are costly to reverse once spread around. The result is that the React Ink TUI is just a client. From the engine’s point of view, another TUI, a desktop app or a web UI would be the same contract with a different screen, and MCP is already a second real client of that same boundary.

Complexity, AI, and the role of the harness

Structure has a cost, and that cost has become a topic: there are recent studies and discussions about experienced developers getting slower with AI on repositories they already knew well, even while estimating the opposite, about agent resolve rates dropping when a change crosses many files, and about models degrading as context grows. Hence a critique that has been gaining ground: every layer is one more file to read, every indirection one more hop to trace, every purposeless abstraction noise competing for attention. Humans never paid per token to understand code, agents do.

The critique makes sense where it points at ceremony. An interface with a single implementation forever, a use case that merely organizes, a mapper between identical DTOs: if the honest answer to “what real problem does this layer solve” is “organization”, the cost is not justified, for people or for models. No pattern in DevStation is exempt from that question.

Where it stops making sense, for me, is when it becomes a general rule. Here, every layer answers the question that guided the design. The ports exist because the provider is swappable by decision, and because the TUI and MCP are already two clients of the same contract. The bounded contexts exist because the domain genuinely accumulates rules. And I have a hypothesis on top of that: a well-delimited context shrinks the set of files an agent needs to load to touch a feature. In DevStation this seems to reduce the agent’s ambiguous decisions, but it is the experience of one project, and I do not yet consider it enough to become a rule.

What holds all of this up in practice is the harness plus verification. The boundaries stopped depending on me remembering every rule during a change: the rules are loaded by the agents, and architecture tests can be used to break the build when a boundary is crossed. The cost of keeping the discipline has not disappeared, but it dropped enough to stop being the main argument against this kind of architecture.

What I can say so far

Back to the rename from the opening. What I can say today is that, in this project, broad refactors have been cheap and safe, and that the cost of the structure has shown up where I expected it to: more indirection to read, more files to touch for a small feature. Whether that trade keeps holding as DevStation grows is too early to tell. So far, every large change that cut across the whole project went like the rename: laborious, but predictable. None of this makes the approach right for any system, and in a smaller project most of these layers would be overkill. For this one, with the direction I have planned for it, the bet stands.

References