DUXPLIMA Documentation

Architecture

Hexagonal layers, CQRS commands, and why the API looks the way it does.

uxmClaims is a hexagonal (ports and adapters) application with a CQRS split between reads and writes. Understanding the shape explains why the API is a facade taking command objects rather than a bag of setters.

The modules

ModuleContainsDepends on
uxmclaims-domainThe model (Claim, ClaimMember, ClaimRole, the enums, the events)nothing
uxmclaims-corePolicies and rules, such as permission resolutiondomain
uxmclaims-appThe facades, the command objects and the portsdomain, core
uxmclaims-infraThe repositories (jOOQ against the database)app
uxmclaims-platform-bukkitEverything Bukkit: commands, menus, listeners, integrationsall

The Bukkit module is split further into api, bootstrap, handlers and infra. api is the part you compile against.

Nothing in domain knows Bukkit exists. That is what makes the rules testable without a server.

Ports and adapters

A port is an interface the application needs. An adapter implements it for a specific platform.

PortAdapters
EconomyPortVaultEconomy, NoopEconomy
PermissionPortLuckPermsPermission, NoopPermission
RegionPortWorldGuardRegion, NoopRegion
PlaceholderPortPlaceholderAPIPort, NoopPlaceholderPort
MapVisualizerPortDynmapVisualizer, BlueMapVisualizer, Pl3xMapVisualizer, SquareMapVisualizer, NoopMapVisualizer

Every port has a Noop implementation. That is why the plugin runs with none of its soft dependencies installed: the missing integration is not a special case, it is an adapter that does nothing.

Commands and queries

Reads and writes go through different shapes.

Queries are plain methods returning data:

Optional<Claim> claim = claimFacade.findByChunk(chunk);
List<Claim> owned    = claimFacade.findByOwnerUid(uuid);
int count            = claimFacade.countByOwnerUid(uuid);

Commands are objects describing an intent, handed to the facade:

ClaimRenameCommand command = ClaimRenameCommand.builder()
        .claimId(claim.getId())
        .actorUid(player.getUniqueId())
        .name("New name")
        .build();

Claim renamed = claimFacade.renameClaim(command);

The command carries the actor, which is what lets one code path enforce permissions, charge the economy, fire the event and post the webhook, rather than every caller remembering to.

Side effects

Every write has an overload taking ClaimCommandOptions, which selects which side effects run:

public enum ClaimSideEffect {
    REGION, ECONOMY, WEBHOOK, LIMITATION, PERMISSION
}
EffectControls
REGIONWorldGuard overlap checks
ECONOMYCharging the player
WEBHOOKPosting to Discord
LIMITATIONEnforcing entitlement limits
PERMISSIONEnforcing ability and role permissions
claimFacade.createClaim(command, ClaimCommandOptions.builder()
        .without(ClaimSideEffect.ECONOMY)
        .without(ClaimSideEffect.WEBHOOK)
        .build());

ClaimCommandOptions.all() is the default, and none() disables everything.

This is how an admin tool, a world generator or a migration script creates claims without charging anyone, spamming Discord or tripping a limit, without reimplementing the write path.

none() also disables the permission check

ClaimCommandOptions.none() turns off PERMISSION and LIMITATION along with the rest. That is correct for a migration and wrong for anything driven by a player. Prefer builder().without(…) and remove exactly the effects you mean.