Architecture WASM

Why I stopped writing the same business logic four times

You build a pricing formula. It takes a product, a customer tier, a quantity, maybe a coupon code. It works. You ship it to production and it calculates exactly what you want.

Then someone asks for it in the mobile app. Fine, you port it. Then the backend worker needs to apply the same formula during batch jobs. You add it there too. Then the AI pipeline needs to call it when generating quotes. Now you have four implementations in four different codebases, probably in three different languages.

Which one is correct? They all started from the same source. But they diverged. Maybe one was updated when a new discount rule shipped. Maybe two were. You do not know. Your tests do not know. Your customers will find out.

The problem nobody talks about

Logic duplication is invisible until it hurts. It does not show up in code review. Nobody flags it in a PR. The individual decision to copy the formula each time was reasonable. The accumulation is the problem.

The reason it is hard to see: each copy lives in context. The browser version handles display formatting. The backend version handles database transactions. The worker version handles batch retry logic. The AI version handles prompt construction. The pricing formula itself is buried inside all of that context. To a reviewer it looks like pricing logic. It is actually four distinct code paths that happen to implement the same rule.

When the rule changes, you update the one you know about. You file a ticket for the others. Some of those tickets get done. Some do not.

Why it happens

It happens because most architectures assume a deployment environment before they define behavior.

You write a React component and the pricing logic lives inside it because the component needed it. You write a Node service and the pricing logic lives in a utility module because that was convenient. The logic was never designed to exist independently. It was always a detail inside something larger.

This seems fine at first. It is not fine at scale. Once the logic is embedded in a runtime context, extracting it requires rewriting it. So teams do not extract it. They copy it instead.

The solutions people try

The obvious answer is a shared library. Put the pricing logic in a package, publish it, import it everywhere. This works until it does not. Shared libraries create version fragmentation. Mobile uses v1.2, backend uses v1.4, worker uses v1.4-beta. You are back to divergence, just with version numbers instead of file copies.

Another approach is a shared service. Extract the pricing formula to a dedicated microservice and call it over the network. This works. It also adds latency, adds a network dependency, and means the mobile app cannot work offline. You have traded a logic problem for an infrastructure problem.

Feature flags are sometimes used to manage environment-specific behavior inside shared logic. This is arguably the worst outcome. Now the pricing formula has conditionals that say things like: if we are on mobile, skip validation; if we are on backend, log to the audit table. The logic and the environment are completely entangled.

What contracts-first actually changes

The contracts-first approach inverts the relationship between logic and environment.

Instead of writing logic inside an environment, you define a capability: a named unit of behavior with a machine-readable contract that says what it does, what it needs, and where it can run. The environment is not baked in. Placement is declared separately, as a deployment decision.

The capability does not know whether it is running in a browser or a worker. It does not need to. It declares its constraints and behavior. The runtime reads the contract, decides where to place it, and runs it there.

This is not a new idea in principle. It is how microservices were supposed to work before they got tangled up in network topology. The difference is that Traverse makes it literal: the capability is a WASM binary, the contract is a structured file, and the runtime actually enforces the placement rules.

What this looks like in Traverse

Here is a capability contract that declares placement targets. The pricing formula itself is compiled to WASM once. The contract tells the runtime where it is allowed to run.

pricing.capability.toml
[capability] id = "pricing.quote-calculator" version = "2.1.0" description = "Calculates a quoted price for a given product and customer tier" [contract] inputs = ["product_id: string", "tier: CustomerTier", "quantity: u32", "coupon: Option<string>"] outputs = ["quoted_price: Decimal", "applied_discounts: Vec<Discount>"] [placement] targets = ["browser", "edge", "cloud", "ai-pipeline"] preferred = "edge" offline = true # can run without network when placed in browser [wasm] module = "./pricing_calculator.wasm" memory = "4mb"

The WASM binary is compiled once from the Rust implementation. The runtime reads the contract and places the capability according to the targets and preferred fields. If the edge is unavailable, it falls back to cloud. If the app is offline and the device has the binary cached, it runs in the browser. The pricing logic itself never changes.

The AI pipeline calls the same capability through the MCP interface. The AI agent does not call a different function with different semantics. It calls the same contract, gets the same output guarantees, and the runtime logs the same trace artifact.

The key insight

The environment should adapt to the logic. Not the other way around.

Every time you write a new version of the pricing formula for a new environment, you are doing the opposite. You are forcing the logic to adapt to wherever it currently needs to live. The logic becomes a passenger in your infrastructure decisions.

Defining the capability first, with a contract that is independent of any deployment context, flips the relationship. The infrastructure reads the contract and arranges itself around the capability. The logic stays stable. The environments come and go.

It takes a bit of adjustment to think this way. The instinct is to write logic where it is needed. The contracts-first approach asks you to write logic where it belongs, and let the runtime figure out where it is needed.

Once you do it for one capability, the rest follows naturally.

Write the logic once.

Define a Traverse capability with a contract and run the same binary across every environment your product needs.

Get started → View on GitHub