# Design Pattern Solidity: Off-chain beats on-chain

Author: Markus Waas

Published: 2019-09-14T07:00:37.000Z

Updated: 2026-09-13T14:25:30.000Z

Source: [https://soliditydeveloper.com/design-pattern-solidity-off-chain-beats-on-chain](<https://soliditydeveloper.com/design-pattern-solidity-off-chain-beats-on-chain>)

## Compatibility and review

Before you start

These are schematic Solidity fragments. Off-chain computation only preserves trust when the contract verifies the submitted result or intentionally trusts its submitter; view calls from another contract still consume gas.

[Official reference](<https://docs.soliditylang.org/en/latest/types.html>)

As you might have realized, Ethereum transactions are anything but cheap. In particular, if you are computing complex things or storing a lot of data. That means sometimes we cannot put all logic inside Solidity.

Instead, we can utilize off-chain computations to help us. A very simple example would be:

---

**Don't**

```solidity
function storeSum(uint256 a, uint256 b) public {
  storedSum = a + b;
}
```

**Do**

```solidity
function storeSum(uint256 _storedSum) public {
  storedSum = _storedSum;
}
```

---

You compute `a + b` before sending out the transaction. Very simple. This tradeoff only applies when the contract can cheaply verify the result or intentionally trusts the submitter and it can be sometimes quite challenging finding the most efficient way to do more off-chain.

## Sorted Ranking Example with Off-Chain Sorting

Let's look at a more complex example. This was something I actually did recently. The requirement was to have a ranking for users inside the smart contract. At first glance that might sound very easy, but a ranking means having a sorted array. And keeping a sorted array of length `n` means on average for a new insertion an added complexity of `n/2` (for finding the correct insert position).

At best, we have only a small ranking, then this just means it costs quite a bit more gas for every insertion. At worst, we have a large ranking and might even get out-of-gas exceptions, rendering a serious security risk for the contract.

**Solution**

- Using a [circular linked list](<https://github.com/modular-network/ethereum-libraries/blob/master/LinkedListLib>), we can easily insert and remove users in the list, but we need to modify the library to use a different mapping (`mapping (address => mapping (bool => address)) list`).
- Having a separate mapping for the user points `mapping (address => uint256) public userPoints`.
- Pre-computing the sorted spot for a newly inserted user - you might have guessed it - off-chain.
- Only verifying the correctness of the suggested insert position when inserted a new user.

I won't go into code examples, as this will get quite long and messy. But I will give you the ideas and outline each function:

**getSortedSpot**

**Trust boundary:** The simple sum example only makes sense when storing a value supplied by the caller is the intended behavior. If the sum affects balances, rewards or authorization, the contract must verify the computation or a suitable proof. Moving a calculation off-chain does not make its result trustworthy.

```solidity
function getSortedSpot(address _user, uint256 _points) public view returns (address)
```

This will be a view function inside your contract. It iterates from bottom to top through your ranking linked list while in each iteration comparing the user points of the current list address to the given `_points`. Once you find the first address in the list that has more points, return it as reference. You will call `getSortedSpot` before inserting a new user to find out the correct insertion position.

Things to consider:

- Don't return a reference address that equals `_user`. As this means an existing user in the ranking will get a new position.
- If there are no addresses with more points, we are dealing with the new user becoming the first rank. Return the current first rank as reference.

**insertUser**

A read-only RPC call pays no transaction fee, but a `view` function called from another contract still executes and consumes gas. An off-chain traversal also has practical RPC limits. The reference can become stale before insertion, so the transaction must recheck the neighboring entries.

```solidity
function sortedInsertUser(address user, address referenceUser) public
```

This will be your actual insertion method. You pass the result from `getSortedSpot` as `referenceUser`. And now we can just verify that the reference is indeed correct:

1. Compute the new points for `user` based on whatever your metrics are.
2. Compare the computed points to those of `referenceUser`. The referenced user must have more points.
3. Compare the computed points to those of one rank below `referenceUser`. One rank below must have less points.
4. Insert the user into the ranking.

Things to consider:

- The first and last rank require special consideration.
- If the passed `user` already exists in the ranking, remove it before newly inserting it or you will get double entries.

Define a deterministic tie rule for equal scores, check that the suggested neighbors really belong to the list, and validate both ordering and links before updating them. Strict comparisons alone reject ties. The signatures above outline the design; they do not implement a complete tested ranking.
