> For the complete documentation index, see [llms.txt](https://docs.ohalee.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ohalee.com/products/networkeconomy/architecture.md).

# Architecture

This page explains **why** NetworkEconomy is safe against balance duplication and lost writes. If you only want to install it, go to [installation.md](/products/networkeconomy/installation.md).

## The problem it solves

The naive way to write an economy plugin is:

1. On join, read the player's balance from the database into a local field.
2. Mutate that field while they play.
3. On quit, write the field back.

On a single server this mostly works. On a network it is broken in two directions:

* **Duplication.** A player with 1,000 coins spends them on the lobby, then quickly rejoins on survival — which still holds the stale cached 1,000 — and quits, writing 1,000 back over the correct value.
* **Lost writes.** Two servers both hold a cached balance and both save it. The last save wins and the other server's earnings vanish.

## The four rules

### 1. The database is the only authority

There is exactly one source of truth: the `balances` table in MySQL / MariaDB. Local caches are **read-through only** — they are never written back as if they were authoritative. A cache miss is just a database read; a cache being wrong can only ever cause a briefly stale *display*, never a wrong *transaction*, because every mutation is evaluated by the database itself.

### 2. All money movements are atomic relative operations

No operation ever reads a balance into Java, changes it, and writes it back. Instead:

| Operation       | How it is executed                                                                                        |
| --------------- | --------------------------------------------------------------------------------------------------------- |
| **Deposit**     | `UPDATE balances SET balance = balance + ? WHERE uuid = ? AND currency = ?`                               |
| **Withdraw**    | `UPDATE balances SET balance = balance - ? WHERE uuid = ? AND currency = ? AND balance >= ?`              |
| **Transfer**    | Both sides inside a single transaction, rows locked with `SELECT ... FOR UPDATE` in a deterministic order |
| **Set / Reset** | `UPDATE balances SET balance = ?` inside a transaction, then read back                                    |

The withdrawal is the important one. The sufficiency check (`balance >= ?`) and the debit are **a single statement**, so they cannot be interleaved. If twenty servers try to withdraw 10 from a balance of 100 at the same instant, exactly ten succeed; the rest affect **zero rows** and return `INSUFFICIENT_FUNDS`. There is no window in which two servers can both "see enough money" and both spend it.

Transfers lock both rows in a deterministic order, which is what prevents two opposite transfers (`A → B` and `B → A`) from deadlocking each other.

{% hint style="info" %}
Balances are stored as `DECIMAL(20,4)` and handled as `BigDecimal` in Java — never `double` — so money arithmetic is exact and no rounding error can accumulate.
{% endhint %}

### 3. A monotonic version column makes sync idempotent

Every balance row carries a `version` counter that is incremented on every change. Redis broadcasts carry `(balance, version)`, and the cache applies an incoming update **only if its version is newer** than what it already holds.

This is what makes the sync layer robust rather than merely fast:

* Messages that arrive **out of order** are harmless — the older one is dropped.
* Messages that arrive **twice** are harmless — applying them is idempotent.
* A message that is **lost entirely** costs nothing but a brief staleness, resolved by the next read or the cache expiry.

### 4. Redis pub/sub keeps caches coherent in real time

When a balance changes, the originating node publishes a compact message on the configured channel. Every other node applies it to its cache (subject to the version guard) and fires `BalanceUpdateEvent` with `isRemote() == true`. The publishing node ignores its own messages by matching `server-id`, which is why that value **must be unique per server**.

If Redis is disabled or fails to connect, the plugin logs it and falls back to re-reading online players' balances from the database every `cache.fallback-refresh-seconds`. Correctness is unaffected — only the propagation delay changes.

## Request flow

```
/pay on survival-2
      │
      ▼
PreTransactionEvent (async, cancellable)
      │
      ▼
SqlStorage.transfer()  ── single DB transaction, FOR UPDATE in fixed order
      │                    (atomic; either both sides apply or neither)
      ▼
transactions table  ── append-only audit row per side
      │
      ├─► local cache updated (version-guarded)
      ├─► BalanceUpdateEvent (local)
      └─► Redis publish ──► every other node
                                 │
                                 ├─► cache updated if version is newer
                                 └─► BalanceUpdateEvent (remote = true)
```

## The Vault trade-off

Vault's `Economy` API is **synchronous** — it must return a number immediately. NetworkEconomy's core is asynchronous. The bridge therefore blocks briefly on the underlying async operation, bounded by a 5-second timeout, so that it can return an accurate response rather than a guess.

Reads are cheaper than they look: the bridge serves `getBalance` from the local cache when the player is cached (the normal case for online players) and only falls back to a blocking database read for offline players.

{% hint style="warning" %}
Latency-sensitive plugins — anything calling the economy in a tight loop or on every tick — should use the native async API instead of the Vault bridge. See [for-developers.md](/products/networkeconomy/for-developers.md).
{% endhint %}

## Startup order

The plugin wires itself in a fixed order, because parts of it depend on each other:

`config` → `storage` (connect + apply schema) → `cache` → `service` → `Redis` → `Vault` → `commands & listeners`

The service is created *before* Redis so the Redis message handler can call back into it, and Redis is then injected into the service. If the database cannot be reached, or the currency configuration is invalid, the plugin **disables itself** rather than running in a state where balances could be wrong.

## Connection handling & shading

Connection handling is built on [DatabaseProvider](https://github.com/ohAleee/DatabaseProvider): pooled MariaDB via HikariCP, Redis via Lettuce with a dedicated pub/sub connection, and SQL schema loading with `{prefix}` substitution.

All bundled libraries — DatabaseProvider, HikariCP, the MariaDB driver, Lettuce, Netty, Reactor — are relocated under `com.ohalee.networkeconomy.lib`. Relocating Netty in particular is **required, not cosmetic**: Paper ships its own Netty and Lettuce needs a newer one, so an unrelocated build would clash with the server at runtime.

## Project layout

```
com.ohalee.networkeconomy
├── NetworkEconomyPlugin          # bootstrap / wiring
├── api/                          # public API (stable surface for other plugins)
│   ├── NetworkEconomyAPI         #   async service interface
│   ├── NetworkEconomyProvider    #   static accessor
│   ├── Currency, EconomyResult, TransactionRecord, BalanceEntry, TransactionType
│   └── event/                    #   PreTransactionEvent, BalanceUpdateEvent
├── core/                         # service implementation
│   ├── EconomyServiceImpl        #   orchestrates storage + cache + redis + events
│   ├── CurrencyManager
│   └── cache/                    #   version-guarded in-memory cache
├── storage/                      # persistence (on top of DatabaseProvider)
│   ├── EconomyStorage, SqlStorage (atomic SQL), SchemaResource
│   └── MutationResult, TransferResult, BalanceRow
├── sync/                         # Redis pub/sub (RedisManager, BalanceMessage)
├── vault/                        # VaultEconomyProvider bridge
├── command/                      # /balance /pay /baltop /eco /transactions
├── gui/                          # transaction-history GUI
├── listener/                     # join/quit cache lifecycle
├── config/                       # PluginConfig, Messages
└── util/                         # Text, NumberUtil
```
