Anvil
Architecture

Module boundaries

Locate the owning Gradle family and keep dependencies pointed toward public contracts.

Begin in the module that owns the behavior. anvil-api describes global lifecycle and registration; a family's API describes its services and extension points. The launcher binds those scoped contracts.

Family or moduleOwns
anvil-apiGlobal engine registration, scenario definitions/lifecycles, public process/player handles, and capability ownership
anvil-engineScenario structure, the process-property contract, setup hooks, active contexts, retention of process groups, and overall cleanup ordering
anvil-environment/cacheIndependent cache API plus filesystem entry coordination and staged publication
anvil-environment/provisioningSeparate artifact, Java, and workspace APIs with acquisition, installation, layout, snapshots, and retention policy
anvil-environment/execution/execution-managedExecution plans, preparation handoff, readiness, consoles, generation replacement, and ordered finalization
Other anvil-environment/execution modulesExecution-provider contracts, local processes, and Docker execution
anvil-process/process-commonDistribution validation, effective Java/topology requirements, forwarding, and provider preparation/configuration
anvil-process/process-apiPlatform-provider contracts, version data, artifact sources, and configuration contracts
anvil-process/process-platform/platform-<name>One family per platform: its provider and, where it installs one, its platform agent assembly
anvil-player/player-common/srcPlayer registration, per-player library and release selection, authentication compatibility, and observations
anvil-player/player-apiProtocol library contracts, releases, players, protocol-owned channels and native worker contracts
anvil-player/player-workerWorker runtime kit every library embeds: extension discovery and binding, session generations, and the protocols a player speaks
anvil-player/player-protocol/protocol-javaThe Java Edition protocol (protocol-java-api) and its worker kit (protocol-java-worker), which dispatches the protocol over a library's connection
anvil-player/player-library/library-mcprotocolThe MCProtocolLib library: release data, client port, client segments, worker host and shell, and account storage
anvil-capability/capability-apiGeneric composition, neutral player contracts, descriptors, exceptions, and typed requests
anvil-capability/capability-commonDependency validation, agent-provider adaptation and agent extensions, logical process owners, player facades, and capability cleanup
anvil-capability/capability-player/capability-player-apiProtocol-backed player providers/contexts, channels/events, and native worker bindings
anvil-capability/capability-player/capability-player-builtinPlayer capability families: feature APIs with their channel contracts, host providers, and Java Edition worker extensions
anvil-capability/capability-process/capability-process-apiAgent-backed process providers and scoped request-channel contexts
anvil-capability/capability-process/capability-process-builtinProcess capability families with agent-backed host wiring: Dispatch, and Players, which reports what one server or proxy knows about the players on it from its own agent's events
anvil-capability/capability-defaultAggregate of every built-in capability
anvil-process/process-protocol/process-protocol-apiShared operation descriptors, payloads, identities, and exceptions
anvil-process/process-protocol/protocol-host/protocol-host-apiHost clients, connections, directories, and artifact lookup
anvil-process/process-protocol/protocol-host/protocol-host-commonConnections, stable process clients and sessions, directories, and observations in the Anvil JVM
anvil-process/process-protocol/protocol-agent/protocol-agent-apiNative services, operation handlers, and embedded endpoint contracts
anvil-process/process-protocol/protocol-agent/protocol-agent-commonEmbedded native operation dispatch, transport endpoint, and external-handler loading
anvil-launcherGlobal engine builder, default scenario factory, scoped-service bindings, and shaded assembly; no rules of its own
anvil-integration/integration-junitJUnit lifecycle, context injection, and outcome propagation
anvil-toolingNeutral session contracts, extensible actions, and foreground/IDE runner
anvil-integration/integration-gradleShared Gradle declarations and explicit scenario, JUnit, and provider-selection adapters
anvil-integration/integration-intellijIntelliJ plugin assembly: descriptor composing the feature modules, settings group, branding, and packaging
anvil-integration/integration-intellij/intellij-apiContracts the IntelliJ features and build integrations share
anvil-integration/integration-intellij/intellij-commonStandard implementations of the shared IntelliJ services, and shared test fixtures
anvil-integration/integration-intellij/intellij-scenarioScenario catalog, environments, consoles, run configurations, and editor run actions
anvil-integration/integration-intellij/intellij-accountProject and global accounts, pools, sign-in, and account directories
anvil-integration/integration-intellij/intellij-gradleNative Gradle adapter
anvil-integration/integration-gradle/gradle-toolingNative Gradle task discovery, definition scanning, artifact binding, and prepared-launch production
anvil-testkit/testsCross-module runtime and live assertions
anvil-testkit/fixturesIndependent consumer build producing real fixture JARs
anvil-testkit/supportShared host-side artifact access and extension classloader lifetime
build-logicJava, testing, assembly, and publication conventions
build-logic/settingsLean settings conventions: library registry, library repositories and library layout checks

The tooling API exposes predefined wire operation contracts at its root: ScenarioOperations, EnvironmentOperations, and ProcessOperations. model.ToolingOperation pairs request and response schemas, and shared request payloads live beside their related models. The runner binds these contracts to behavior through ToolingOperationRegistry; its reader has no operation-specific decoding methods.

IntelliJ is a frontend in the logical tooling API family. The integration-intellij project declares architecture.family = projects.anvilTooling.path, which its modules inherit. Its physical location remains under anvil-integration. The tooling runner executes in a separate project JVM; the plugin never depends on its implementation.

IntelliJ plugin modules

The plugin is split by feature. The family root is the plugin itself and holds no logic: its plugin.xml includes one descriptor fragment per module and declares the Settings → Tools → Anvil group, and its build packages the modules with the plugin icons.

integration-intellij/
  src/                plugin.xml (includes the fragments below), settings group, branding
  intellij-api/       contracts the features and build integrations share
  intellij-common/    anvil-common.xml: their standard implementations; shared test fixtures
  intellij-scenario/  anvil-scenario.xml: catalog, environments, consoles, run configurations, editor
  intellij-account/   anvil-account.xml: accounts, pools, sign-in, account directories
  intellij-gradle/    anvil-gradle.xml: the Gradle build integration, loaded only with the Gradle plugin

Feature modules are implementations, so the architecture verifier lets them depend only on APIs. A feature reaches shared behaviour through a service interface in intellij-api, bound by the module that implements it; it never imports another feature. intellij-api holds:

ContractImplemented byUsed for
BuildIntegration extension pointintellij-gradleDiscovering, syncing, and preparing scenario sources of one build system
BuildIntegrationsintellij-common (ProjectBuildIntegrations)All installed integrations as one project service
ToolingCommandsintellij-common (ManifestToolingCommands)Reading a prepared launch manifest into runner and sign-in commands
AccountStaging, StagedAccountsintellij-account (ProjectAccountStaging)Giving a scenario run a private copy of the project's accounts
ScenarioSource.DATA_KEYScenario viewsTelling actions contributed by other features which source a view shows

Features also meet through the IDE's action system rather than through code. The scenario module registers its actions and the groups that show them (Anvil.Catalog.Toolbar, Anvil.Catalog.Popup, Anvil.Environment.Toolbar, Anvil.Environment.Popup); the account module adds Anvil Accounts… to the catalog toolbar from its own fragment. The root plugin.xml includes the scenario fragment before the account fragment, so the group exists when the account action joins it.

Scenario module

intellij-scenario groups its packages by what they own, each with a view subpackage where it has Swing screens. Concrete classes are project or application services (@Service); only the shared contracts above are interfaces.

PackageResponsibility
toolingToolingHost arbitrates the project's single tooling launch and its Purpose; ToolingLaunch owns preparation, the runner, staged accounts, and cleanup and publishes one Outcome; ToolingConnection owns the child processes. tooling.protocol holds ToolingClient and ToolingMessageCodec.
catalogScenarioCatalog loads the selected source's scenarios into one ScenarioCatalog.Snapshot. catalog.source.SourceDiscovery owns detection, selection, and sync, delegating reload-after-import decisions to ImportRefreshPolicy. catalog.view holds CatalogPanel, its CatalogController, and the scenario tree.
environmentEnvironmentLifecycle owns the active environment and the retained sessions, and decides through availability whether a scenario or process can run and why not. Each EnvironmentSession is bound to one launch through EnvironmentExecution and reports an EnvironmentSession.Status. environment.view holds the session tab with its overview, console, players, and contribution pages.
actionThe registered actions: run, stop, restart, save as run configuration, reload, and sync. They read ScenarioDataKeys and ScenarioSource.DATA_KEY from the invoking view and describe why they are unavailable.
editorScenarioLineMarker puts a Run icon beside definition classes recognised through UAST by ScenarioDefinitions.
runconfigurationAnvilRunConfiguration stores a source, a definition class, and an optional process; ScenarioConfigurationProducer creates one from a class; RunConfigurationService saves, validates, and launches them through EnvironmentLifecycle.
log, consoleSessionLog retains neutral output; each ConsolePresentation renders it in one native console. CommandHistory keeps accepted commands per CommandHistory.Scope.
navigationDefinitionNavigator and WorkspaceNavigator open definition sources and process directories.
settingsScenarioSettings and its Scenarios settings page.
windowThe tool-window factory, ToolWindowController for its tabs, retention, and failure notifications, ScenarioPresentation, and shared view components.

Views offer their selection through UiDataProvider and show registered action groups; they never decide what a command does. Every entry point to running — the catalog, the editor gutter, run configurations, and environment tabs — goes through EnvironmentLifecycle.run, or through the tab's own EnvironmentSession when it already runs the scenario, so all of them follow the one-environment rule and give the same reasons.

The account module follows the same split: AccountLibrary reads the project and global directories, storage owns the account and pool files and staged copies, AccountSignIn runs one browser sign-in, settings holds AccountSettings and the Accounts settings page, and view holds the account window.

Panels and dialogs are written in Kotlin, with Kotlin UI DSL for forms; controllers, services, and platform adapters are Java in the matching packages. Kotlin targets the API level of the baseline IDE's bundled standard library, which the module-intellij convention sets for every module that applies the Kotlin plugin. The feature modules apply Kotlin's Lombok plugin so views can use members Lombok generates for Java classes.

examples/proof-of-patience is a standalone consumer build. It consumes published artifacts and stays outside root project discovery. Framework system assertions belong in anvil-testkit.

Locate a capability or agent extension

Capability ownership and transport are separate. Capability and CapabilityOwner are global identity/lookup contracts. PlayerCapability and ProcessCapability constrain the types accepted by players and running processes. Their APIs do not require an agent transport.

Built-in families are grouped by participant, decided by who can normally do or know it, never by how it is implemented. capability-player/capability-player-builtin contains what a client can do or see, which players own, such as Session and Messages. capability-process/capability-process-builtin contains what a server or proxy can do or know, which running processes own, such as Dispatch and Players; a proxy is server side. The groups are no public owners themselves: public feature packages and artifact IDs remain feature-specific, such as builtin-players.

Each built-in capability is a family folder builtin-<feature>: <feature>-api holds the public API and, for a player capability, the contract between host and worker in its channel package; <feature>-common holds the host provider in the feature packages; and a player capability's <feature>-java holds its worker extension, which speaks the Java Edition protocol through the player's JavaProtocol. The library's client segments translate that protocol for each release, so no built-in capability has segments of its own. The family root applies the module-capability convention and only wires these members into the builtin-<feature> bundle. The convention publishes the members as builtin-<member>, such as builtin-movement-api, and refuses a child that is not a member, a common or util package, and code in the root of a family with common code or sides.

A family that reads its processes through their agents, such as dispatch or players, keeps its provider in <feature>-common as well: it uses the platform agents' general operations, so it ships nothing into the processes.

Capability providers share CapabilityProvider, CapabilityContext, and CapabilityDescriptor from capability-api. Its RequestChannel performs a process's typed calls described by ChannelOperation; a player's channel uses player-api's own OperationDescriptor, because capability APIs never reach into the player family's contracts the other way. The shared capability.api.player package contains PlayerCapabilityProvider and PlayerCapabilityContext: player identity/version, the requested and granted profiles, the connection, declared dependencies, and lifetime.

The mechanism APIs add only the access their providers need. capability-player-api groups protocol player factories and their context under capability.protocol.api.player; the context hands a provider the player's ProtocolChannel from player-api. The player family owns that channel and the worker contracts: player-api holds WorkerExtension, PlayerBindingContext, OperationRegistry and WorkerBinding under protocol.api.worker, OperationDescriptor<Q, R> and EventDescriptor<E> under protocol.api.model.channel, and PlayerConnectionEvent and ViewRotation under protocol.api.model.player. A descriptor pairs an ID with the classes of its payloads; publishers and subscribers share it, and each emitted event is a separate payload value. capability-process-api groups native request factories under capability.agent.api.process. Both mechanism APIs depend on capability-api, without exposing the other mechanism or an agent transport API.

Agent transport and native handlers have separate contracts. agent-client-api, in agent.client.api, contains clients, connections, directories, and artifact lookup. agent-server-api, in agent.server.api, contains AgentOperationProvider, native platform service access, and transport registration. Both receive operation descriptors and payloads from agent-api, in agent.api. The client implementation and native handler run in different JVMs.

The launcher packages agent-client; platform-agent artifacts package the agent-server implementation published as agent. Launcher bindings supply request channels to capability-owned agent factory adapters. Those adapters participate in neutral capability composition without exposing agent transport APIs.

The agent role APIs depend on their explicitly shared ancestor agent-api, but cannot depend on one another. Neither role nor the shared agent API depends on anvil-api; libraries can consume agent transport and handler contracts independently of engine and capability contracts. The host implementation uses global player observations where needed without exposing them through client APIs.

Locate a protocol library

A protocol library is a family under anvil-player/player-library/library-<library>, and its folder holds the library's release data, <library>-releases.toml. The settings plugin build-libraries registers every such library. MCProtocolLib's family is laid out like this:

ModuleOwns
library-mcprotocolWiring-only root: shades mcprotocol-common, publishes protocol-mcprotocol, and owns mcprotocol-releases.toml and its pinLibraryReleases task
mcprotocol-apiThe release-neutral client port McProtocolClient<S> that client segments implement, and what their translations share: PacketTranslation, the PacketTable that carries a release's translations, and EntityDataLayout
mcprotocol-adventureAdventureText, which flattens Adventure components to plain text for the segments of releases whose components are Adventure's (MCProtocolLib for Minecraft 1.16 on). Adventure 5 renamed a translation's arguments, so the earliest segment of each chain creates it with the accessor its releases share; Adventure itself comes from each release's closure
mcprotocol-commonMcProtocolLibraryProvider, the release catalog, worker host and segment selection, the worker shell, and the private account store
mcprotocol-javaSource-free side folder whose V* children are the client segments
mcprotocol-java/V1_16_5, V1_17_1, V1_18_2, V1_19_4, V1_20_6, V1_21_1, V1_21_11, V26_1_2 and V26_3One client segment per range of releases. V1_16_5 starts the chain of the client and server packet names, V1_18_2 the chain of the clientbound and serverbound names in the com.github.steveice10 packages and V1_20_6 the chain of the org.geysermc packages, each with the chain's public JavaClient base; V1_17_1 extends V1_16_5, V1_19_4 extends V1_18_2, V1_21_1 extends V1_20_6, V1_21_11 extends V1_21_1, V26_1_2 extends V1_21_11 and V26_3 extends V26_1_2, replacing the base classes that changed. Every segment has a McProtocolClientAdapter listing its release's translations

The worker shell never imports MCProtocolLib. Every class that does lives in a segment: a project named after the release key it starts at, under a side folder named after its owner and library, such as mcprotocol-java or an external capability's teleport-mcprotocol. A segment holds only the adapters of library-neutral ports and their service descriptors; it never wires anything. Its package ends in the folder name in lower case, such as .v1_18_2. For a release, the worker keeps the segment of each side with the greatest start version that does not exceed the release key. A new segment is added only when a release breaks the code of the previous one, which checkSegmentLinkage reports; see Adding a Minecraft version.

A capability that needs packets the protocol does not model can still repeat the pattern: its API declares a port in its packet package, and its <feature>-mcprotocol side holds the wiring extension and the V* segments that implement it; see Protocol adapters.

Preserve dependency direction

anvil-api has no Anvil project dependency, and the engine depends only on anvil-api. Ordinary implementations and family APIs may depend on their own family's APIs and anvil-api. They must not import another family's API through either a direct dependency or an API re-export.

anvil-environment groups directories; it does not combine cache, execution, artifacts, Java, and workspaces into one API family. Cache operations remain in cache-api, HTTP acquisition in artifact-api, Java installation in java-api, and workspace lifecycle in workspace-api. The engine has no dependency on these APIs. Concrete SDKs, serialization, and transports also stay outside the global contract.

When another family needs part of a service, define a focused input or preparation boundary in the consumer's API. For example, execution requests a local executable through LocalRuntimePreparation and guards image metadata through ImageLocks; launcher adapters bind those requests to Java and cache APIs. Docker keeps its image selection, cache layout, and validation order.

Implementations depend on APIs and external libraries, not sibling implementations. Introduce a shared API only for a real extension or module boundary. Ordinary collaborators such as a configuration writer and a distribution resolver remain concrete within their owning implementation.

Assembly modules can bind and package implementations. Keep service construction in the launcher and external integrations in anvil-integration. Classifying an ordinary implementation as an assembly does not resolve a misplaced dependency. Service descriptors belong with the implementation or assembly that supplies the service, and shading must merge them. Feature wiring bundles register their providers and worker extensions; the native feature implementation consumes its own API and actual SDK.

Consume a scoped API directly

cache-api and the artifact API published as provisioning-api have no dependency on anvil-api. A library can consume them without constructing an engine or installing the launcher.

The published cache-filesystem implementation supplies cache-api transitively. For a Java application, use this build fragment with your chosen anvilVersion property and the usual Maven repositories:

plugins {
	application
}

val anvilVersion = providers.gradleProperty("anvilVersion").get()

dependencies {
	implementation("me.whereareiam.anvil:cache-filesystem:$anvilVersion")
}

application {
	mainClass.set("CachedText")
}

Put this complete program in src/main/java/CachedText.java and run ./gradlew run. It publishes a staged entry and prints hello after reopening it:

import me.whereareiam.anvil.environment.cache.api.model.CacheKey;
import me.whereareiam.anvil.environment.cache.filesystem.FileCache;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public final class CachedText {
	public static void main(String[] args) throws IOException {
		var cache = new FileCache(Path.of("build", "example-cache"));
		var key = CacheKey.builder().namespace("example-text").value("greeting").suffix(".txt").build();
		try (var entry = cache.open(key); var write = entry.stageFile()) {
			Files.writeString(write.path(), "hello");
			write.commit();
		}
		try (var entry = cache.open(key)) {
			System.out.println(Files.readString(entry.path()));
		}
	}
}

Neither cache artifact requires anvil-api or an engine. A library that accepts a caller-supplied Cache can depend on cache-api alone. Entry leases own coordination, and staged writes own publication. An artifact consumer similarly borrows ArtifactResolver from its own API; it does not need to know which storage implementation the resolver uses.

Keep packages navigable

Use feature-oriented packages. Keep contracts at the feature root, reusable public values under model, and enums or closed value types under type. Physical directories match package names. Small implementation-local carriers can be inner records; public models use separate top-level files. A package needs at least two files unless it is one of the fixed role packages model, type, exception, and packet. A capability with segments keeps its release port in packet, even when the port is the package's only file, so the port has the same place in every family. A segment's version package, such as v1_18_2, and a library side's wiring package, such as teleport.mcprotocol, hold one class by design and are exempt as well. Avoid catch-all util or common packages; common code lives in the feature packages.

Within workspace provisioning, directory owns confined file access and layout, preparation owns plan validation and prepared sessions, and snapshot owns snapshot identity and storage. The root DefaultWorkspaceProvisioner coordinates those responsibilities. Tests mirror the production packages; immutable layout and preparation contracts remain in workspace-api.

Use imports, JetBrains nullability annotations, and useful multiline Javadocs on public APIs. Prefer immutable values and Lombok for routine construction. Java source uses tabs; dependencies and other shared build behavior follow build-logic conventions and the grouped version catalog.

Verify a boundary change

Update project dependencies, public imports, service filenames, descriptor contents, and published artifact wiring together. Run the owning module's tests and ./gradlew verifyArchitecture. For a packaging change, also verify runtime discovery and a consumer build against the produced artifacts.

The architecture task inspects production project dependencies, including inherited configurations, API exports, and runtime or embedded dependencies. It does not inspect external Maven artifacts or prove public signatures and runtime behavior correct. Review those boundaries alongside the check.

On this page