Skip to content

Storage

frosxt edited this page Apr 14, 2026 · 1 revision

Storage

PrisonCore picks one storage backend at boot based on core.yml and exposes that backend to every module through a single registry. Modules don't pick their own backend — the platform-wide selection is the contract, and your module either supports the active backend or it doesn't. In practice this is fine because all four supported backends (JSON, SQLite, SQL/MySQL, MongoDB) cover the common cases.

The kernel manages the lifecycle (connect, disconnect, health checks). Your module asks for the active backend, casts it to the concrete class for the type it expects, and uses the underlying handle directly.

StorageRegistry

com.github.frosxt.prisoncore.kernel.storage.StorageRegistry

Resolved through the service container:

final StorageRegistry storage = context.services().resolve(StorageRegistry.class);

The relevant methods:

public StorageBackend getOrCreate(String type, String scope, Map<String, Object> config);
public StorageBackend getOrCreate(String type, Map<String, Object> config);
public Optional<StorageBackend> getActive(String type);
public Optional<StorageBackend> getActive(String type, String scope);
public boolean hasFactory(String type);
public Set<String> availableTypes();
public void registerFactory(StorageBackendFactory factory);

getOrCreate(type, scope, config) is the workhorse. It looks up the active backend for type:scope, creates and connects it on first call, and returns it on subsequent calls. The scope argument lets two modules share a backend type without colliding — for example, getOrCreate("sql", "economy", ...) and getOrCreate("sql", "skills", ...) get independent connection pools.

getOrCreate(type, config) is shorthand for the unscoped (default) instance.

registerFactory(factory) adds a new backend type. You only call this from a PRE_INFRASTRUCTURE module that contributes a custom backend implementation.

CoreConfig

com.github.frosxt.prisoncore.kernel.config.CoreConfig

The active backend is configured platform-wide in core.yml:

storage:
  backend: sqlite

You read it via CoreConfig, which is registered in the service container:

final CoreConfig coreConfig = context.services().resolve(CoreConfig.class);
final String backendType = coreConfig.storageBackend();
final Map<String, String> settings = coreConfig.storageProperties();

storageProperties() returns every key under storage in core.yml except backend itself, so SQL credentials, the Mongo URI, sqlite file path, and so on all flow through here.

StorageBackend

com.github.frosxt.prisoncore.spi.storage.StorageBackend

The minimal contract for any backend:

public interface StorageBackend {
    String name();
    void connect() throws Exception;
    void disconnect();
    boolean isHealthy();
}

You don't call these methods yourself. The registry calls connect() on first acquisition and disconnect() during kernel shutdown. You get the backend already connected.

To do anything useful with the handle you cast it to the concrete class for the active type.

The four built-in backends

Each backend is a top-level class with a typed accessor for its underlying handle. The pattern is the same in every case: getOrCreate, instanceof-check, cast, use.

JsonStorageBackend

com.github.frosxt.prisoncore.storage.json.JsonStorageBackend

public Path directory();

The "handle" is just the root data directory. You write your own files underneath it with whatever schema you want.

final StorageBackend backend = storage.getOrCreate("json", "hello", Map.of("directory",
    context.dataFolder().resolve("data").toString()));
if (backend instanceof final JsonStorageBackend json) {
    final Path file = json.directory().resolve("greetings.json");
    // read or write the file
}

SqliteBackend

com.github.frosxt.prisoncore.storage.sqlite.SqliteBackend

public Connection connection();

Owns a single JDBC connection to a file-based database. The connection is shared, so don't close it. Use it directly for prepared statements:

if (backend instanceof final SqliteBackend sqlite) {
    try (final PreparedStatement ps = sqlite.connection().prepareStatement(
            "INSERT INTO greetings(player_id, text) VALUES (?, ?)")) {
        ps.setString(1, playerId.toString());
        ps.setString(2, text);
        ps.executeUpdate();
    }
}

The file config key controls the database file path. If you don't want users picking the location, hardcode it relative to your module data folder when you build the config map.

SqlBackend

com.github.frosxt.prisoncore.storage.sql.SqlBackend

public Connection connection() throws SQLException;

Backed by a HikariCP connection pool. Each call to connection() borrows a connection from the pool. You must close it (try-with-resources) to return it.

if (backend instanceof final SqlBackend sql) {
    try (final Connection conn = sql.connection();
         final PreparedStatement ps = conn.prepareStatement(...)) {
        // ...
    }
}

The pool is sized via the pool-size setting in core.yml. Default url is jdbc:mysql://localhost:3306/prisoncore.

MongoBackend

com.github.frosxt.prisoncore.storage.mongo.MongoBackend

public MongoDatabase database();

Returns the connected MongoDatabase from the official Mongo Java driver. From there you use the driver normally:

if (backend instanceof final MongoBackend mongo) {
    final MongoCollection<Document> collection = mongo.database().getCollection("greetings");
    collection.insertOne(new Document("playerId", playerId.toString()).append("text", text));
}

StorageBackendFactory

com.github.frosxt.prisoncore.spi.storage.StorageBackendFactory

Only relevant if you are contributing a new backend type from a PRE_INFRASTRUCTURE module.

public interface StorageBackendFactory {
    String type();
    StorageBackend create(Map<String, Object> config);
}

Register it during your onPrepare:

context.services().resolve(StorageRegistry.class)
    .registerFactory(new MyCustomBackendFactory());

Your factory's type() is the string users select in core.yml. Your factory's create() builds an unconnected backend instance — the registry calls connect() on it before handing it out.

Repository contracts

platform-storage includes generic repository contracts you can use to abstract over storage in your own code. They aren't required — you can write directly against the backend handle — but they're useful when you want to swap implementations cleanly.

com.github.frosxt.prisoncore.storage.api.Repository<ID, AGGREGATE> — synchronous CRUD-ish surface (find, findAll, save, delete, exists, count).

com.github.frosxt.prisoncore.storage.api.AsyncRepository<ID, AGGREGATE> — same shape with CompletableFuture returns. Prefer this when called from the main thread.

com.github.frosxt.prisoncore.storage.api.DocumentStore<ID, D> — schemaless document store with predicate-based filtering. Lighter than Repository.

com.github.frosxt.prisoncore.storage.api.KeyValueStore<K, V> — minimal key/value contract. Use when you don't need queries.

com.github.frosxt.prisoncore.storage.api.LedgerStore<K, E> — append-only log of entries grouped by key. Use for transaction journals and audit trails.

com.github.frosxt.prisoncore.storage.core.AbstractCachingRepository<ID, AGGREGATE> — base class implementing Repository and AsyncRepository with a write-through in-memory cache. Subclass it and implement the four backend hooks (loadFromBackend, saveToBackend, deleteFromBackend, idOf).

These are tools, not requirements. If you'd rather write directly against Connection or MongoDatabase, that's fine.

Lifecycle responsibilities

You don't need to disconnect or shut down the backend. The kernel does that on its own during shutdown.

You do need to flush any in-memory state of your own before the kernel disconnects the backend. Cancel any scheduled flush task you have, then drain any pending writes synchronously in your onDisable:

@Override
protected void onDisable(final ModuleContext context) {
    if (flushTask != null) {
        flushTask.cancel();
    }
    myCache.flushSynchronously();
    // backend disconnects automatically after all modules disable
}

If you skip the synchronous flush and rely on the periodic task, you risk losing the writes that arrived in the last interval before shutdown.

Clone this wiki locally