← Back to Blog

One Graph, Four Native Runtimes: Preserving Architecture Without Erasing the Language

A language-agnostic architecture should not produce language-agnostic code. It should preserve the system's intent while letting every runtime speak in its own voice.

Architectural intentOne typed graphcontracts · topology · error paths · observability
validateIRgenerate
GoBackend services
PythonFast iteration & ML
C++Native & low latency
RustSafety & predictable performance

In an earlier article, I argued that an AI agent needs something more useful than a repository dump: a compact architectural representation shared with the developer. At the time, generating the same architecture for multiple languages was still a direction rather than evidence.

That experiment now has four working targets: Go, Python, C++, and Rust. The generated projects are available as ordinary repositories: goexample, pyexample, cppexample, and rustexample. The interesting result is not that a generator can print four kinds of syntax. Templates can do that. The interesting question is what can remain genuinely common when the execution models are different.

How much architectural meaning can survive a change of language without reducing every runtime to the lowest common denominator?

The Test Case Is a System, Not “Hello World”

A trivial CRUD endpoint proves very little. I used a more revealing example: an order service accepts an order, starts a soft-deadline path, splits the order into items, processes them concurrently, calls an inventory service over gRPC, keeps success and error results explicit, maps them into order state, and merges the branches into one response.

This graph contains architectural decisions worth preserving: fan-out and fan-in, service boundaries, request and response types, a deadline policy, and separate failure paths. None of those decisions inherently says “goroutine,” “Python coroutine,” “C++ task processor,” or “Rust future.”

What the Graph Actually Owns

The graph is not a visual translation of source code. It owns the invariants that should remain true in every implementation:

  • Topology: which operations exist and how data moves between them;
  • Typed contracts: what each port accepts and produces;
  • Invocation semantics: local execution, external calls, fan-out, merging, and delays;
  • Failure semantics: errors are named paths, not accidental exceptions hidden in a call stack;
  • Operational identity: the same nodes become stable points for configuration, metrics, and traces;
  • Change boundaries: generated infrastructure and handwritten business logic have explicit ownership.

Those are architectural facts. The graph validates them before a language backend is selected. Only after that does the generator decide how to express them.

typed portstopologysuccess / errorconfigtraces

Common Semantics, Native Mechanisms

The goal was never to make four languages look the same. A generator that emits Java-shaped Go, Go-shaped Python, or C++ disguised as Rust has preserved syntax while losing the reason to choose the language.

Instead, the architectural operation is translated into the idiomatic mechanism of its target runtime.

GoBackend services

goroutines · context · interfaces

go test View generated project
PythonFast iteration & ML

async tasks · type hints · protocols

pytest · mypy · Ruff View generated project
C++Native & low latency

coroutines · RAII · task processors

CMake · CTest View generated project
RustSafety & predictable performance

futures · ownership · traits

cargo test View generated project

A parallel branch remains a parallel branch, but the implementation uses the runtime's own scheduling model. A typed port remains a typed port, but it may become a struct and interface, type annotations and protocols, native C++ types, or Rust structs and traits. Resource lifetime and cancellation remain part of the behavior, but they are expressed through the mechanisms developers of that ecosystem already understand.

This distinction matters:

shared:  architectural semantics
native:  execution mechanics

same graph ≠ same source code
same graph = same architectural intent

One Architecture Does Not Mean One Language

The language is selected per service, not necessarily for the entire project. A single application graph can therefore describe a real polyglot system.

In the order-processing example, the Order Service can be generated in Go while the Inventory Service is generated in Rust. The boundary between them still comes from the same typed graph: request and response contracts, the gRPC connection, error paths, configuration, and trace relationships are generated consistently on both sides.

The same topology could use Python for a service that wraps an ML model, or C++ for a latency-sensitive component. Each generated service remains an ordinary independent project with its own build system and runtime. They are not mixed inside one binary; they communicate through explicit, generated contracts.

This is where a shared architectural model becomes more useful than a set of unrelated generators. It preserves the topology and cross-service contracts of the whole system while allowing every service to use the runtime that fits its workload.

Why This Is More Than Four Template Sets

If the input model merely contained language-specific fragments, multi-language generation would be a routing exercise. A meaningful IR has to survive before any backend knows the final syntax.

That forces a useful separation. The architecture describes what must be true; each backend decides how that truth is implemented and verified. The generated project is not just a collection of function stubs. It includes the service structure, connectors, configuration, observability wiring, build files, tests, and small local tasks for an AI agent.

The verification chain is native too. Go is checked with its toolchain, Python with tests plus static and style checks, C++ through its build and test system, and Rust through Cargo. “Generation succeeded” is not enough; the output has to be a normal project in its own ecosystem.

The Unexpected Benefit: Better Boundaries for AI

Supporting several languages made the AI use case clearer. An agent working on business logic does not need a universal prompt containing every implementation detail of all four runtimes. It needs two compact layers of context:

  1. the language-independent contract of the selected node or component;
  2. the language-specific task: file, signature, local conventions, and verification commands.
Shared contractReserveInventoryOrderItem → Reserved | Unavailable | Failure
Local taskImplement one functiontarget language + file + checks

This is especially valuable in large systems. The agent does not have to reconstruct the entire architecture from a mountain of code before touching one function. The graph supplies the boundary; the generator supplies a small, executable specification for the selected runtime.

In other words, multi-language support did not make the AI contract broader. It made the split between architectural context and implementation context more explicit.

What “Language-Agnostic” Does Not Mean

It does not mean that all four services have identical performance, allocation behavior, failure modes, or deployment profiles. It does not mean that every library in one ecosystem has a perfect equivalent in another. And it does not mean that a developer can ignore the runtime after choosing it.

The abstraction promises something narrower and more useful: the same architecture can be validated, reviewed, generated, and observed consistently, while the resulting code remains recognizable to a developer who works in that language.

There must also be escape hatches. Unusual algorithms, specialized libraries, low-level optimizations, and domain-specific integrations still belong in handwritten code. A constrained architectural language is valuable precisely because it does not try to absorb the whole programming language.

A Better Definition of Portability

We often call software portable when the same source code can run on another platform. At the architectural level, a different definition may be more useful:

Portable architecture preserves intent, contracts, and operational structure—even when its implementations are intentionally different.

That is what the four-runtime experiment demonstrated for me. The architectural graph can remain stable without flattening Go, Python, C++, and Rust into an artificial common language. The IR carries the decisions that belong to the system. The generator translates those decisions into code that belongs to the runtime.

The practical implementation behind this work is gorundebug/servicelib. It generates working service projects and local SDD tasks for AI agents across the four supported languages.

The next question is no longer whether one graph can produce several languages. It is what this shared execution structure costs compared with direct code. I measured that in a generated-versus-native performance analysis.