Understanding Maven's Internal Session Management: Architecture, Thread Safety, and API Usage
Maven coordinates build lifecycles through a three-layer session architecture that tracks execution state, manages thread-local project contexts, and provides service lookup to plugins while maintaining immutability through derived sessions.
Maven's internal session management is the backbone that coordinates multi-module reactor builds, tracks execution outcomes, and supplies repository services to plugins. In the apache/maven repository, this system spans from the concrete MavenSession implementation in the legacy core to the modern API-first Session interface introduced in Maven 4. Understanding these components reveals how Maven achieves thread-safe parallel builds, isolates project state across threads, and enables extensible plugin data sharing.
The Three-Layer Session Architecture
Maven's session model operates at the intersection of three distinct layers, each serving different compatibility and abstraction needs.
MavenSession (Legacy Core API)
The org.apache.maven.execution.MavenSession class serves as the concrete execution-time representation used by the core and legacy plugin APIs. Located in impl/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java, this class holds the immutable MavenExecutionRequest, MavenExecutionResult, and the Eclipse Aether RepositorySystemSession. It maintains thread-local state through the currentProject field, allowing plugins to query which project is currently being processed by the executing thread.
Session (Maven 4 API Contract)
Introduced in Maven 4, org.apache.maven.api.Session provides an abstraction that shields plugin authors from internal implementation details. Defined in api/maven-api-core/src/main/java/org/apache/maven/api/Session.java, this interface exposes operations for project list retrieval, repository handling, artifact resolution, and listener registration. The DefaultSession implementation in impl/maven-impl/src/main/java/org/apache/maven/internal/impl/DefaultSession.java delegates service lookups to the underlying dependency injection container while providing derived session capabilities.
Internal Implementation Classes
Beneath the public APIs lie several critical implementation classes that provide the actual thread-safe storage and session lifecycle management:
AbstractSession– Base class providing common plumbing for listener management and property handlingDefaultSession– Concrete Maven 4 implementation that wraps the repository system and DI containerInternalSession– Bridge class connecting legacyMavenSessionoperations with the new APISessionScope– Manages the lifecycle of@SessionScopedcomponents, ensuring single-instance-per-build semantics
Key Session Concepts and State Tracking
The session maintains several distinct state containers that coordinate the build execution.
Execution Request and Result
The Maven execution request (MavenExecutionRequest) provides immutable configuration data including goals, properties, and repository settings. Upon build completion, the session populates a Maven execution result (MavenExecutionResult) containing exceptions and the build success flag.
Repository System Session
At the Aether level, the RepositorySystemSession manages remote and local repository configurations, mirrors, proxies, and authentication. This session is wrapped by the Maven session and exposed to plugins requiring direct repository access.
Thread-Local Current Project
To support parallel builds, MavenSession uses a ThreadLocal<MavenProject> field named currentProject. When setProjects(List<MavenProject>) is called, the first project initializes the thread-local holder. Each thread processing a reactor project clones the session via MavenSession.clone() to obtain its own isolated ThreadLocal holder, preventing race conditions during multi-threaded execution.
Plugin Contexts
Plugins store execution state across multiple mojo invocations using plugin contexts. Internally, MavenSession maintains a ConcurrentMap structure keyed by projectId → pluginKey → userMap. The getPluginContext(PluginDescriptor, MavenProject) method lazily creates these nested maps, guaranteeing a non-null concurrent map for each project/plugin combination.
Derived Sessions
Sessions support immutability-preserving derivation through methods like withLocalRepository(LocalRepository) and withRemoteRepositories(List<RemoteRepository>). These return new Session instances that wrap the original but override specific repository configurations, allowing plugins to customize resolution behavior without affecting the global build state.
Session Lifecycle and Build Coordination
Understanding how Maven constructs and propagates sessions reveals the build coordination strategy.
Build Startup and Session Construction
- Request Creation: Maven creates a
MavenExecutionRequestfrom CLI arguments andsettings.xmlconfiguration - Repository Session Initialization: The core constructs an Eclipse Aether
RepositorySystemSessionwith configured mirrors and proxies - MavenSession Instantiation: The constructor wires together the request, result, repository session, and execution properties (merged system and user properties)
public MavenSession(
RepositorySystemSession repositorySystemSession,
MavenExecutionRequest request,
MavenExecutionResult result) {
this.request = requireNonNull(request);
this.result = requireNonNull(result);
this.repositorySystemSession = requireNonNull(repositorySystemSession);
Properties executionProperties = new Properties();
executionProperties.putAll(request.getSystemProperties());
executionProperties.putAll(request.getUserProperties());
this.executionProperties = executionProperties;
}
Thread Safety and Project Isolation
When MavenSession.setProjects() initializes the reactor, it establishes the thread-local project context:
public void setProjects(List<MavenProject> projects) {
if (!projects.isEmpty()) {
MavenProject first = projects.get(0);
this.currentProject = ThreadLocal.withInitial(() -> first);
this.topLevelProject = projects.stream()
.filter(project -> project.isExecutionRoot())
.findFirst()
.orElse(first);
}
this.projects = projects;
}
During parallel builds, each worker thread clones the session to receive its own ThreadLocal holder while sharing the immutable request and result data.
Plugin Context Initialization
When a mojo executes, it retrieves its context map through:
public Map<String, Object> getPluginContext(PluginDescriptor plugin, MavenProject project) {
String projectKey = project.getId();
ConcurrentMap<String, ConcurrentMap<String, Object>> pluginContextsByKey =
pluginContextsByProjectAndPluginKey.computeIfAbsent(projectKey,
k -> new ConcurrentHashMap<>());
String pluginKey = plugin.getPluginLookupKey();
return pluginContextsByKey.computeIfAbsent(pluginKey,
k -> new ConcurrentHashMap<>());
}
This lazy initialization ensures thread-safe storage for plugin state that must survive across multiple executions of the same plugin within the same build.
Thread Safety and Session Scope
Maven introduced session-scoped components using the @SessionScoped annotation to guarantee single-instance-per-build semantics. The SessionScope class in impl/maven-core/src/main/java/org/apache/maven/session/scope/internal/SessionScope.java manages a map keyed by the Session instance, ensuring that only one component instance exists for the entire build while allowing safe access from multiple threads.
This differs from project-scoped components (per-project instances) and singletons (JVM-wide instances), providing a middle ground for services that should be shared across the reactor but isolated between separate Maven invocations.
Practical Code Examples
Accessing the Current Maven Session in a Mojo
Plugins inject the session using the ${session} expression, available as both the legacy MavenSession and the modern Session API:
@Mojo(name = "show-info", defaultPhase = LifecyclePhase.VALIDATE)
public class ShowInfoMojo extends AbstractMojo {
@Parameter(defaultValue = "${session}", readonly = true, required = true)
private MavenSession mavenSession;
public void execute() throws MojoExecutionException {
getLog().info("Build started at: " + mavenSession.getStartInstant());
getLog().info("Top directory: " + mavenSession.getTopDirectory());
getLog().info("Current project: " + mavenSession.getCurrentProject().getArtifactId());
}
}
Storing Custom Data in Plugin Contexts
Use the plugin context to persist state across multiple mojo executions:
public void execute() throws MojoExecutionException {
Map<String, Object> ctx = mavenSession.getPluginContext(pluginDescriptor,
mavenSession.getCurrentProject());
// Store state
ctx.put("myKey", "someValue");
// Retrieve in subsequent execution
String value = (String) ctx.get("myKey");
getLog().info("Retrieved from context: " + value);
}
Creating a Derived Session with Custom Repository
Plugins can create isolated repository configurations without mutating the global session:
public void execute() throws MojoExecutionException {
Path newRepoPath = Paths.get("/tmp/custom-local-repo");
LocalRepository newLocalRepo = mavenSession.createLocalRepository(newRepoPath);
Session derived = mavenSession.withLocalRepository(newLocalRepo);
// Resolution uses the custom repository
ArtifactCoordinates coords = session.createArtifactCoordinates("org.apache.commons", "commons-lang3", "3.14.0", "jar");
DownloadedArtifact resolved = derived.resolveArtifact(coords);
}
Registering Global Build Listeners
Attach listeners to receive lifecycle events:
public void execute() {
mavenSession.registerListener(new ExecutionListener() {
public void beforeSessionStart(Session session) {
getLog().info("Session about to start");
}
public void afterSessionEnd(Session session, MavenResult result) {
getLog().info("Session finished with success = " + result.isSuccess());
}
});
}
Accessing the Full Project List
Retrieve all projects including those filtered out by --projects:
public void execute() {
List<Project> all = mavenSession.getAllProjects();
getLog().info("Reactor contains " + all.size() + " projects");
}
Resolving Version Ranges with the Maven 4 API
Use the new Session API for version resolution:
VersionRange range = session.parseVersionRange("[1.0,2.0)");
List<Version> resolved = session.resolveVersionRange(
session.createArtifactCoordinates("org.apache.maven", "maven-core", "3.9.0", "jar"),
session.getRemoteRepositories());
Summary
- Three-layer architecture: Maven uses
MavenSession(legacy),Session(Maven 4 API), and internal implementation classes (DefaultSession,SessionScope) to coordinate builds - Thread-safe isolation: The
currentProjectThreadLocaland session cloning enable parallel builds without race conditions - State persistence: Plugin contexts provide
ConcurrentMapstorage keyed by project and plugin, allowing state to survive across mojo executions - Immutability support: Derived sessions via
withLocalRepository()andwithRemoteRepositories()allow repository customization without global mutation - Service abstraction: The
SessionAPI providesgetService()for dependency injection lookup while shielding plugins from container specifics
Frequently Asked Questions
What is the difference between MavenSession and Session in Maven 4?
MavenSession in org.apache.maven.execution is the legacy concrete class holding request, result, and thread-local state, used throughout Maven 3.x and maintained for backward compatibility. org.apache.maven.api.Session is the Maven 4 abstraction that provides a stable contract for plugin development, exposing service lookup, derived sessions, and repository operations while hiding internal DI container details.
How does Maven handle thread safety during parallel builds?
Maven achieves thread safety by cloning the MavenSession for each worker thread, giving each thread its own ThreadLocal holder for the currentProject while sharing immutable data like the execution request and result. The pluginContextsByProjectAndPluginKey uses ConcurrentHashMap structures to ensure safe concurrent access to plugin state storage.
What are plugin contexts and when should they be used?
Plugin contexts are nested ConcurrentMap structures (project → plugin → user data) that allow mojos to store state that must persist across multiple executions of the same plugin within the same build. Use MavenSession.getPluginContext() to retrieve the map for the current project and plugin combination when you need to cache expensive computations or share data between different goals of the same plugin.
How can a plugin create a derived session with custom repository settings?
Plugins call session.withLocalRepository(newLocalRepo) or session.withRemoteRepositories(newRemotes) to create a derived session. This returns a new Session instance that wraps the original but overrides the specified repository methods, preserving the immutability of the original session while allowing customized artifact resolution for specific operations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →