# Best Practices for Structuring Large GenLayer Contracts: A Complete Guide

> Master best practices for structuring large GenLayer contracts. Learn modular design, deterministic storage, and consensus safety for scalable development. Read the complete guide.

- Repository: [GenLayer Labs/genlayer-project-boilerplate](https://github.com/genlayerlabs/genlayer-project-boilerplate)
- Tags: best-practices
- Published: 2026-08-20

---

**Structure GenLayer contracts with dedicated modules, deterministic storage types, encapsulated non-deterministic logic, and strict equivalence principles to maintain scalability and consensus safety.**

GenLayer introduces a unique execution model where Python contracts run in a deterministic-plus-non-deterministic virtual machine (GenVM). As projects grow beyond simple prototypes, architectural discipline becomes critical for consensus correctness, testability, and long-term maintainability. This guide distills proven patterns from the [genlayerlabs/genlayer-project-boilerplate](https://github.com/genlayerlabs/genlayer-project-boilerplate) repository, showing exactly how to organize large GenLayer contracts with concrete code examples and source file references.

## Separate Concerns with Dedicated Contract Modules

Each contract file should own a single responsibility. The boilerplate demonstrates this through clear file boundaries:

- **Domain contracts** like [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py) encapsulate complete business logic for specific use cases
- **Pattern reference contracts** like [`contracts/PatternTest.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/PatternTest.py) serve as living documentation for SDK capabilities

This separation helps the `genvm-lint` tooling catch issues early and makes unit testing straightforward. When a contract grows beyond 200 lines, consider splitting storage structures and helper logic into separate internal modules.

## Use Explicit Deterministic Storage Types

GenLayer only persists VM-compatible containers. Avoid Python native types like `list` or `dict` in contract state. Instead, use these built-in primitives from the GenLayer SDK:

| Type | Purpose | Example from Source |
|------|---------|-------------------|
| `TreeMap[Key, Value]` | Sorted key-value mappings | `bets: TreeMap[Address, TreeMap[str, Bet]]` |
| `u256` | Unsigned 256-bit integers for counters/balances | `points: TreeMap[Address, u256]` |
| `DynArray[T, N]` or `Array[T, N]` | Fixed or dynamic ordered collections | Use instead of Python `list` |

In [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py) (lines 22-25), the storage declaration follows this pattern precisely:

```python
class FootballBets(gl.Contract):
    bets: TreeMap[Address, TreeMap[str, Bet]]

```

Nested `TreeMap` structures enable complex relationships—here, bets grouped first by user address, then by bet identifier—without sacrificing determinism.

## Leverage @allow_storage for Custom Data Classes

Rich domain objects require explicit opt-in for persistence. Wrap data in `@dataclass` with the `@allow_storage` decorator to signal VM compatibility:

```python
@allow_storage
@dataclass
class Bet:
    id: str
    has_resolved: bool
    game_date: str
    resolution_url: str
    team1: str
    team2: str
    predicted_winner: str
    real_winner: str
    real_score: str

```

This definition appears in [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py) (lines 8-20). The decorator ensures the GenVM can serialize and deserialize `Bet` instances deterministically across all nodes in the consensus set.

## Encapsulate Non-Deterministic Logic in Private Helpers

Web fetches and LLM prompts introduce external variability. Isolate these calls in private methods (no decorator) to enable test mocking and keep public interfaces deterministic:

```python
def _check_match(self, resolution_url: str, team1: str, team2: str) -> dict:
    def get_match_result() -> str:
        web_data = gl.nondet.web.render(resolution_url, mode="text")
        task = f"""
        Extract the match result for:
        Team 1: {team1}
        Team 2: {team2}
        Web content:
        {web_data}
        Respond in JSON: {{ "score": str, "winner": int }}
        """
        result = gl.nondet.exec_prompt(task, response_format="json")
        return json.dumps(result, sort_keys=True)

    result_json = json.loads(gl.eq_principle.strict_eq(get_match_result))
    return result_json

```

This implementation from [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py) (lines 29-55) demonstrates the pattern: an inner closure captures non-deterministic operations, while the outer method applies equivalence principles and returns parsed results.

## Apply Equivalence Principles Consistently

All non-deterministic leader functions must be wrapped with an equivalence principle. The boilerplate uses `gl.eq_principle.strict_eq` to ensure all replicas reach identical conclusions:

```python
result_json = json.loads(gl.eq_principle.strict_eq(get_match_result))

```

This line (lines 54-55 in [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py)) is the critical consensus checkpoint. Alternative principles like `eq_principle.eq` or custom equivalence functions exist for specialized use cases, but `strict_eq` provides the strongest guarantee for JSON-structured data.

## Expose a Minimal Public API

Use GenLayer decorators sparingly and intentionally:

- **`@gl.public.write`** for state-mutating operations: `create_bet`, `resolve_bet`
- **`@gl.public.view`** for read-only queries: `get_bets`, `get_points`, `get_player_points`
- **No decorator** for internal helpers: `_check_match`

This boundary appears throughout [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py) (lines 57-119). Keeping the public surface small reduces attack vectors and simplifies frontend integration.

## Write Deterministic Unit Tests with Direct Mode

The `tests/direct/` directory contains tests that execute contracts locally without network dependencies. Use `direct_vm.mock_web` and `direct_vm.mock_llm` to stub non-deterministic layers:

```python
def test_create_bet(direct_vm, direct_deploy, direct_alice):
    contract = direct_deploy("contracts/football_bets.py")
    direct_vm.sender = direct_alice

    # Mock the web page containing fixture lists

    direct_vm.mock_web(r".*bbc.com/sport/football.*", {
        "status": 200, 
        "body": "<html>…</html>"
    })

    # Mock LLM response for match extraction

    direct_vm.mock_llm(r".*Extract the match result.*", {
        "winner": -1, 
        "score": "-"
    })

    contract.create_bet("2024-10-01", "TeamA", "TeamB", "TeamA")
    assert "teama_teamb" in contract.get_bets()[direct_alice.as_hex]

```

Direct-mode tests validate contract logic in milliseconds, enabling tight feedback loops during development.

## Centralize Configuration in a Single Entry Point

Deployment-specific values belong exclusively in [`config/genlayer_config.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/config/genlayer_config.py). Import this module rather than scattering literals across contracts. This pattern ensures environment parity between local testing, staging, and production deployments.

## Document Patterns in a Living Reference Contract

The [`contracts/PatternTest.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/PatternTest.py) file serves as executable documentation for common SDK patterns:

- `u256` arithmetic and overflow handling
- `Address` comparisons and conversions
- JSON stability techniques
- Nested `TreeMap` workarounds for advanced data structures

Refer to this contract when encountering unfamiliar GenLayer behaviors rather than experimenting in production code.

## Keep Frontend Code Fully Decoupled

The Next.js frontend in `frontend/` consumes contracts through generated TypeScript bindings like [`frontend/lib/contracts/FootballBets.ts`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/frontend/lib/contracts/FootballBets.ts). This abstraction allows contract interfaces to evolve while frontend teams update bindings independently.

## Summary

- **Modular structure** prevents monolithic contracts—separate domain logic, utilities, and patterns into dedicated files
- **Deterministic storage** requires `TreeMap`, `u256`, and `@allow_storage` dataclasses—never use raw Python containers
- **Non-deterministic isolation** through private helper methods enables testability and equivalence principle application
- **Strict equivalence** via `gl.eq_principle.strict_eq` guarantees consensus for all external calls
- **Minimal public API** decorated with `@gl.public.write` and `@gl.public.view` reduces complexity and attack surface
- **Direct-mode testing** with mocked web and LLM layers provides fast, deterministic validation
- **Centralized configuration** in [`config/genlayer_config.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/config/genlayer_config.py) maintains environment consistency

## Frequently Asked Questions

### What storage types does GenLayer support?

GenLayer supports `TreeMap` for sorted mappings, `u256` for unsigned integers, and `DynArray`/`Array` for ordered collections. Python native types like `list` and `dict` cannot be persisted in contract state. The GenVM enforces these restrictions to guarantee deterministic serialization across all consensus nodes.

### How do I make custom classes storable in GenLayer?

Decorate your `@dataclass` with `@allow_storage` before the dataclass decorator. This signals the GenVM compiler to generate serialization logic. All fields must themselves be storable types—nested custom classes each require their own `@allow_storage` annotation.

### Why must non-deterministic logic be isolated?

Non-deterministic operations (web requests, LLM prompts) produce different results across nodes and time. Isolating them in private methods allows wrapping with equivalence principles for consensus, enables mocking in direct-mode tests, and prevents accidental exposure through public APIs. The [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py) implementation demonstrates this pattern in the `_check_match` method.

### What is the equivalence principle and when should I use strict_eq?

The equivalence principle defines how replicas agree on non-deterministic results. `gl.eq_principle.strict_eq` requires bitwise-identical responses—ideal for JSON with sorted keys. Other principles like `eq_principle.eq` allow semantic equivalence for more flexible matching. Apply the strictest principle that your use case permits to maximize consensus safety.