The On-Chain Boundary
Deciding what genuinely belongs on-chain and what is there for narrative reasons. Cost, latency, privacy and immutability as the four axes, the trust that oracles reintroduce, and the ordinary software that makes up most of a working system.
The most consequential architectural decision in a blockchain system is also the one most often made by default. Where does the boundary sit between what executes on-chain and what runs as ordinary software? Get it right and you have a small, well-verified core carrying the guarantees that need guaranteeing, surrounded by a conventional system you can build, test and change at normal speed. Get it wrong and you have an expensive, slow, permanently public, unfixable implementation of something that needed none of those properties.
The failure is usually directional. Things end up on-chain that have no business being there — because the project's framing demands it, because a stakeholder wants to say the system is on-chain, or because nobody drew the line and the default in a blockchain team is to reach for a contract. Every such decision permanently increases attack surface, and, as argued in delivering software you cannot patch, attack surface at a value-bearing address cannot be retracted.
The discipline is to make the boundary an explicit, defended decision, component by component.
The one question that resolves most cases
Before the axes, one test settles a surprising proportion of decisions on its own.
Does this need to be trustlessly verifiable by a party who does not trust you?
That is what a chain actually provides. Not storage, not computation, not distribution — all available more cheaply elsewhere. It provides that a mutually distrusting set of parties can agree on state and on the rules that changed it, without relying on any of them: an expensive, slow, public mechanism for removing the need to trust an operator.
If the answer is yes, it is a candidate. Custody of assets, the rules by which assets move, settlement between parties who do not trust each other, a commitment someone needs to prove you made — these genuinely require the property.
If the answer is no, you are paying for a guarantee you are not using. Application preferences, user profiles, analytics, search indexes, notification state, content, the vast majority of business logic — none of these needs a distrusting third party to verify them. Putting them on-chain is not a stronger version of storing them in a database; it is a weaker version, at higher cost, with worse latency, no privacy and no ability to fix a mistake.
The four axes
Where that question does not settle it, evaluate the component against four properties. They are not preferences; they are the physics of the environment.
Cost. On-chain computation and storage are priced per operation, paid by whoever sends the transaction, and vary with network conditions. Storage is the expensive part, and a design that stores a record per user action has costs that grow with usage in a way ordinary infrastructure does not. Layer-two networks and rollups change the magnitude substantially, but only the magnitude: the ordering holds, and the cheapest transaction is still more expensive than a database write by a wide margin.
Latency. A transaction is not effective when you send it, but when it is included and you consider it sufficiently settled, and how long that takes varies by network and by how much certainty you need. Anything requiring an immediate response — validation as a user types, interactive flows, anything where a person is waiting — cannot have a transaction in the loop. This is not a tuning problem; it is a property of the medium.
Privacy. Everything on a public chain is public, permanently, to everyone — competitors, adversaries, anyone building a profile of an address. Personal data does not belong there, and hashing it is often insufficient where the input space is small or guessable. The legal dimensions vary by jurisdiction and are a question for qualified counsel, but the engineering point stands alone: data that should not be permanently public should not be written to a permanently public medium.
Immutability. The reason the whole thing exists, and the reason to be selective. It is exactly what you want for the rules governing custody and exactly what you do not want for anything you expect to iterate on. A component you intend to improve monthly is a poor candidate for a medium where improvement requires either an upgrade mechanism — with the trust consequences set out in upgradeability is a trust decision — or a migration.
| Component | Trustless verification needed | Cost | Latency | Privacy | Iteration expected | Verdict |
|---|---|---|---|---|---|---|
| Custody of user assets | Yes | Justified | Acceptable | Public is fine | Rare | On-chain |
| Rules for moving those assets | Yes | Justified | Acceptable | Public is fine | Rare | On-chain |
| Settlement between parties | Yes | Justified | Acceptable | Public is fine | Rare | On-chain |
| Business logic and workflow | No | Poor | Poor | Often sensitive | Frequent | Off-chain |
| User profiles, preferences, messaging | No | Poor | Poor | Sensitive | Frequent | Off-chain |
| Search, history and analytics | No | Very poor | Poor | Mixed | Frequent | Off-chain, from indexed data |
| Content and media | No | Prohibitive | Poor | Mixed | Frequent | Off-chain, referenced if needed |
The right-hand column is the shape you are aiming for: a narrow, stable, high-value core on-chain, everything else conventional.
Oracles and the trust they reintroduce
The moment a contract needs to know something about the world outside the chain — a price, a rate, an event — a trust assumption walks back in through the door you were trying to close.
This is worth stating precisely because it is regularly glossed over. Your on-chain logic can be flawless, and if an adversary can influence the value it reads, they can direct that flawless logic to do whatever they want. An oracle is not a detail hanging off the edge of the system; it is frequently the highest-value target in it. The design questions that matter:
Where does the value originate, and can it be moved cheaply? A price from a venue with thin liquidity can be pushed by an adversary for a single transaction, at a cost far below what they extract. Depth of the source is a security property.
Who can write the value on-chain, and what if they are wrong? A single reporting address is a single point of failure; multiple independent reporters with aggregation is better. Either way, define what the contract does when reporters disagree, and make that behaviour safe rather than convenient.
How stale is too stale, and what happens then? A contract with no freshness check will happily act on a number from an outage. Enforce a maximum age and decide deliberately whether exceeding it means pausing, reverting or falling back — silently falling back to a weaker source is what most often turns an outage into a loss.
Can a single transaction both move the source and act on it? If so, you have an atomic manipulation path. Time-weighting, delays and multiple sources are the standard mitigations, each trading manipulation resistance against responsiveness.
What does a sanity bound cost you? Rejecting values outside a plausible range, or limiting how far one may move in a single update, is cheap and catches both manipulation and honest reporting errors. It can also stop a legitimate extreme movement, a trade-off to make consciously rather than by omission.
Treat the oracle as a first-class part of the threat model, with a named actor whose motive is to move the number. An oracle assumption is best tested by running an adversarial price movement against forked state and seeing what your system does — the simulation work described in smart contract security as a delivery practice.
Indexers, relayers and keepers
Three categories of off-chain component appear in nearly every real system. They are ordinary software, they carry substantial operational risk, and they are routinely under-resourced because attention goes to the contracts.
Indexers. Chain state is optimised for verification, not querying. Any interface needing a user's history, a leaderboard, a search or a filtered list needs an indexer: a service that follows events and maintains a conventional database shaped for reading. The practical concerns are unglamorous — reorganisations mean it must unwind and reprocess; a schema change may mean reindexing from genesis, so know how long that takes before you need it; and an index can silently drift from chain state, so reconciliation checks are not optional. Never let an indexer become a source of truth for anything the contracts depend on. It is a cache with a fashionable name.
Relayers. Services that submit transactions on someone's behalf, typically so users need not hold the native token for fees. Ordinary infrastructure with unusual failure modes: they hold keys, so key management is first-order; they need funding, and one that runs out of gas is an outage; they need nonce management under concurrency; and they need rate limiting, because a relayer that will pay for anyone's transaction is a service for draining your own funds. Consider too what a user can do if it is down — a system with no path for a user to submit their own transaction has made the relayer a trusted party without saying so.
Keepers. Anything requiring action at a time or on a condition — liquidations, settlements, rebalances, expiries — needs someone to send a transaction, because contracts do not act on their own. If the keeper fails, the protocol does not do the thing it promised. Run redundant keepers so that one failing is not an incident, and make the on-chain action idempotent and safe under competing callers, because they will race. Design the incentive so that if your keeper fails someone else is motivated to do the job: a permissionless, rewarded action is far more robust than one only you can perform. And monitor for the action not happening, which is harder than monitoring for errors and is the alert you will actually need.
Most of the system is ordinary software
Count the components in a working blockchain product. A front end. Backend services. A database. An indexer. Relayers and keepers. Monitoring. Authentication for the non-chain parts. Support tooling. Administrative interfaces. Analytics. Deployment infrastructure. Then, somewhere in the middle, a few contracts.
By volume the contracts are a small minority; by risk they are disproportionate, which is why the boundary should keep them small. The delivery implication is the one teams consistently miss: most of your system should be delivered with entirely ordinary practices at ordinary speed, and treating the whole programme as though it were governed by contract-deployment constraints is a serious, self-inflicted cost.
Off-chain components ship continuously. Trunk-based development, small batches, continuous deployment, rollback on failure. Everything in trunk-based development and why it matters applies unchanged, and no front-end change should wait for a contract release cycle.
The test strategy is two strategies. A conventional pyramid for the off-chain system, as in test strategy for teams that ship daily, and the heavier invariant and fuzzing regime for the contracts. Applying the contract regime to the front end wastes effort; applying the front-end regime to the contracts is how funds are lost.
Freeze only what must be frozen. During a contract code freeze, off-chain work continues normally. Teams that stop everything for an audit pay a large delivery cost for no risk reduction.
The interface between the two deserves design attention. Contracts emit events; off-chain systems consume them and submit transactions. That boundary is where most operational incidents live — a missed event, a reorganisation handled wrongly, a failed transaction never retried. It is ordinary distributed-systems work and should be resourced as such rather than treated as plumbing.
Staff for the split. A team of only contract specialists builds a weak off-chain system; one with none builds a dangerous on-chain one. Both skills are needed.
What to do on Monday
Draw your system and colour every component by where it runs. Then, for each on-chain component, write one sentence naming the party who would have to trust you if it were not. If you cannot name that party, you have found a candidate to move off-chain, and moving it reduces permanent attack surface at no cost to any guarantee you actually offer.
Take the biggest on-chain component and ask what could be removed from it. Not refactored — removed, to a service, a database, an indexer. Deployed code is permanent code, and this is the cheapest risk reduction available at design time.
Then list your oracles, keepers and relayers and write down what happens when each fails: is the failure safe, is it detected, who is paged. Most teams find at least one component whose failure is silent and whose consequence is that the protocol quietly stops doing something it told users it would do. That gap is easier to close now than during the incident.