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 handling
  • DefaultSession – Concrete Maven 4 implementation that wraps the repository system and DI container
  • InternalSession – Bridge class connecting legacy MavenSession operations with the new API
  • SessionScope – Manages the lifecycle of @SessionScoped components, 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

  1. Request Creation: Maven creates a MavenExecutionRequest from CLI arguments and settings.xml configuration
  2. Repository Session Initialization: The core constructs an Eclipse Aether RepositorySystemSession with configured mirrors and proxies
  3. 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 currentProject ThreadLocal and session cloning enable parallel builds without race conditions
  • State persistence: Plugin contexts provide ConcurrentMap storage keyed by project and plugin, allowing state to survive across mojo executions
  • Immutability support: Derived sessions via withLocalRepository() and withRemoteRepositories() allow repository customization without global mutation
  • Service abstraction: The Session API provides getService() 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →