-
Notifications
You must be signed in to change notification settings - Fork 0
Provider Guide
This guide is for developers building an economy backend: a plugin that stores balances and exposes them through Conduit. If you are adapting an existing economy plugin (EssentialsX, CMI, etc.), read Building a Bridge first; it builds on this page.
Your provider implements so.alaz.conduit.api.economy.Economy (which extends Capable). Every storage-touching method returns a CompletableFuture.
public final class MyEconomy implements Economy {
private final Currency currency =
SimpleCurrency.ofDefault("coins", "$", 2);
@Override public String getName() { return "MyEconomy"; }
@Override public Currency defaultCurrency() { return currency; }
@Override public Set<Capability> capabilities() {
return Set.of(Capability.ECONOMY_PREFLIGHT, Capability.ECONOMY_OFFLINE_PLAYERS);
}
@Override
public CompletableFuture<EconomyResult> deposit(UUID uuid, BigDecimal amount) {
return CompletableFuture.supplyAsync(() -> {
BigDecimal after = creditInStore(uuid, amount);
return new EconomyResult.Success(uuid, currency, after, /* transaction */ null);
});
}
// ... the remaining Economy methods
}You implement the economy behavior: storage, balance math, account lifecycle. You do not implement amount validation, events, interceptors, or caller attribution. The registry wraps your provider in a dispatch layer that adds all of that. Concretely:
- Do not re-check for null/negative/zero/scale-overflow amounts. The dispatch boundary already threw
IllegalArgumentExceptionsynchronously before your method is called. - Do not fire
EconomyTransactionEvent. The dispatch layer fires it post-commit from yourSuccess. - Do not read
CallerTokento decide policy. Interceptors handle pre-auth.
This is why the conformance suite (see Testing & Conformance) tests provider domain behavior, not validation.
Override it if you rely on API features newer than 1.0:
@Override public String requiredApiVersion() { return "1.0"; }The registry refuses to register a provider that requires a newer API than the running runtime (Conduit.API_VERSION), failing fast with a clear message rather than breaking at call time.
Map your outcomes onto the sealed cases. Be honest: do not return Success for a no-op.
| Situation | Return |
|---|---|
| Operation committed | Success(account, currency, newBalance, transaction?) |
| Not enough funds (withdraw/transfer) | InsufficientFunds(balance, requested, currency) |
| No such account | AccountNotFound(uuid) |
| Currency not supported by the account | CurrencyNotSupported(currency) |
| Backend failure (IO, SQL, etc.) | ProviderError(message, cause) |
transaction in Success may be null when the operation is not recorded as a Transaction (for example set()). Rejected is produced by the dispatch layer when an interceptor vetoes; you never return it yourself.
If your backend supports banks, multiple currencies, history, or leaderboards, implement the matching extension interface on the same class. This preserves backend identity: the same instance answers base and extended queries.
public final class MyEconomy
implements TransactionalEconomy, MultiCurrencyEconomy {
// both extension interfaces, one backend
}Each extension is documented in Extension Interfaces. Fine-grained, within-interface capabilities are declared in capabilities() and checked by consumers via supports(...); see Capabilities.
Register once, under your most-derived service type, during your plugin's enable. Registration is order-insensitive; consumers using whenProviderAvailable will pick you up whenever you register.
@Override
public void onEnable() {
MyEconomy economy = new MyEconomy(/* ... */);
Conduit.getRegistry().register(
// most-derived type your provider implements
TransactionalEconomy.class,
economy,
this, // your Plugin
ServicePriority.Normal
);
}-
Most-derived registration. Register a provider under the single most-derived type it implements. If it implements several sibling extensions (say
TransactionalEconomyandBankingEconomy) with no common subtype, either declare a composite interface in your own module that extends both, or pick one type to register under. The registry does not guess. -
Hierarchy-walking resolution.
getProvider(Economy.class)resolves any provider registered under a subtype. A provider registered asTransactionalEconomyis found by queries for bothTransactionalEconomyandEconomy. -
Priority tie-break. When multiple providers satisfy a query, highest
ServicePrioritywins; within equal priority, earliest registration wins. Subtype specificity is not a tiebreaker. - No duplicate-instance registration. The same instance cannot be registered twice. To expose one backend under several interfaces, implement them all on one class and register under the most-derived type; the hierarchy walk does the rest.
-
Swaps fire
ActiveProviderChangeEventper base service key when the active provider for that key changes.
Unregister on disable:
@Override
public void onDisable() {
Conduit.getRegistry().unregister(TransactionalEconomy.class, economy);
}- Do real IO off the main thread. Return a future that completes when the work is done; do not block inside the method.
- On Folia, respect region threading for any Bukkit-side work. Pure storage logic can run on your own executor.
- Idempotent operations (
TransactionalEconomy) must be safe to retry. See the idempotency contract in Extension Interfaces; a reusableIdempotencyStorehelper lives inso.alaz.conduit.api.economy.support.
so.alaz.conduit.api.economy.support.AbstractEconomyProvider provides scaffolding for common provider chores so you implement less boilerplate. Use it if it fits; it is not required.
Before shipping, run your provider against the conformance fixtures in conduit-test-fixtures:
class MyEconomyConformanceTest extends AbstractEconomyConformanceTest {
@Override protected Economy createEconomy() {
return new MyEconomy(/* fresh, isolated instance */);
}
}There are matching base classes for banking, multi-currency, and transactional providers. Green conformance is the bar for a trustworthy provider. Full detail: Testing & Conformance.
- Implements
Economy(and any extension interfaces on the same class) -
getName()is stable and unique enough for operators to pin viaprovider-override -
capabilities()is honest - Returns correct
EconomyResultcases, includingInsufficientFundsandProviderError - Does not re-validate amounts, fire events, or read caller tokens
- Registers under the most-derived type, once, with a sensible
ServicePriority - Unregisters on disable
- Passes the conformance suite
Conduit is a modern Economy abstraction for Minecraft (Paper/Folia, Java 25). Released under the MIT License. · Docs · Developer Guide · GitHub
Start here
For server operators
Concepts
For plugin developers (consumers)
For backend developers (providers)
Reference