namalzi

← All posts

Digging into Gradle's Source 4: The Service Registry — the DI Container Inside Gradle

Gradle · Published

Who puts a FileSystemOperations into your task when you mark its constructor with @Inject? This post reads DefaultServiceRegistry, one per scope, which looks services up through its parents; the rules for declaring services with @Provides methods and decorators; lazy singletons created on first lookup; ServiceScopeValidator, which keeps services in their proper scope; GradleModuleServices, through which 57 modules add services to every scope; and the _Decorated classes that fill in abstract getters. We use the registry directly outside a build and print the stack of a task being created to check it all against the source.

What this post covers

This is the fourth post in the "Digging into Gradle's Source" series. In part 3 we followed the gradle command as it finds or starts a daemon, hands over the build request, and the daemon starts the build through its command chain. Inside that daemon, the objects doing the work (the cache that remembers file hashes, the factory that creates tasks, the service that resolves dependencies) need to find each other. How? This post reads the answer: the service registry.

Back on the map from part 2: part 2 drew the hierarchy of state (build process → session → build tree → build → project) and the service scopes that pair one-to-one with it. This post opens the "box of services, one per scope" from that picture. What does a box look like, how are boxes connected, and how does the task class you wrote get services out of a box? Those are the questions.

Item Details
Subject Gradle Build Tool
Version 9.10.0 development build (master)
Commit 07ecbf9ead (2026-10-24)
Main classes read DefaultServiceRegistry, ServiceRegistryBuilder, ServiceRegistrationProvider·@Provides, ServiceScopeValidator, GradleModuleServices, DependencyInjectingInstantiator, AsmBackedClassGenerator

All line numbers in this post refer to this commit. Part 1 explains how to get the same commit and build the Docker image used for the examples.

The core file DefaultServiceRegistry.java alone is 1,495 lines, and AsmBackedClassGenerator.java, which creates user classes, is 2,145 lines. This post sticks to the main thread. The parts left out are listed by name and location in "What this post leaves out" at the end.

Terms to know first

A service is a long-lived object inside Gradle. It does one job: "compute file hashes", "create tasks", "record build operations", and so on. Usually one interface (FileHasher, ObjectFactory, …) is the service's type, and other services find it by that type. It plays the same role as a bean in Spring.

A service registry is the box that holds services and hands them out by type. Its interface is ServiceRegistry, and get(type), find(type) and getAll(type) are nearly all of it (ServiceRegistry.java lines 27–70). There is a single implementation, DefaultServiceRegistry. It sits where Spring's BeanFactory sits (Spring part 3), and throughout the source it simply shows up as a variable named services.

A service scope is a compartment for services grouped by how long they live: for the whole life of the daemon (Global), while the same Gradle user home (~/.gradle) is in use (UserHome), for one command (BuildSession), for one build (Build), for one project (Project), and so on. Scopes match the hierarchy of state from part 2 one-to-one, and one registry is created per scope.

A service registration provider is a class that adds services to a registry. A createXxx() method annotated with @Provides declares "this is how to make a service of this type". Think of Spring's @Configuration classes and @Bean methods. Classes whose names end in …Services, such as GlobalScopeServices, BuildScopeServices and ProjectScopeServices, are usually providers.

A decorator takes a service that already exists in the parent registry, wraps it, and offers the result as a new service of the same type. It is used when a child scope wants a slightly modified version of the parent's service.

A generated class is a subclass that Gradle creates at run time with ASM (a bytecode generation library). When you write abstract class Probe : DefaultTask(), Gradle creates Probe_Decorated, fills in the abstract getters, and actually uses an instance of that. We already saw this in part 1's example under the name Build_gradle$Hello_Decorated.

1. The big picture: one registry per scope, lookup through parents

Registries are made with a builder (ServiceRegistryBuilder). Here is the Global registry, the first one the daemon creates on startup.

        ServiceRegistryBuilder builder = ServiceRegistryBuilder.builder()
            .scopeStrictly(Scope.Global.class)
            .displayName("Global services")
            .provider(new GlobalScopeServices(longLiving, agentStatus, currentGradleInstallation))
            .provider(new BuildProcessScopeServices());
        for (ServiceRegistry parent : parents) {
            builder.parent(parent);
        }
        for (ServiceRegistrationProvider serviceProvider : serviceProviders) {
            builder.provider(serviceProvider);
        }
        services = builder.build();

platforms/core-runtime/build-state/src/main/java/org/gradle/internal/buildprocess/BuildProcessState.java lines 46–57

The builder takes four things: which scope (scopeStrictly), a name (displayName), the providers that contribute services (provider), and the parent registries (parent). The registry of a single project has the same shape.

    public static CloseableServiceRegistry create(
        ServiceRegistry buildServices,
        ProjectInternal project,
        LoggingManagerFactory loggingManagerInternalFactory
    ) {
        return ServiceRegistryBuilder.builder()
            .scopeStrictly(Scope.Project.class)
            .displayName("project services")
            .parent(buildServices)
            .provider(new ProjectScopeServices(project, loggingManagerInternalFactory))
            .provider(new WorkerSharedProjectScopeServices(project.getProjectDir()))
            .build();
    }

subprojects/core/src/main/java/org/gradle/internal/service/scopes/ProjectScopeServices.java lines 118–130

The parent is buildServices, the registry of the build this project belongs to. BuildSessionState makes the session registry, BuildTreeState the build tree registry and AbstractBuildState the build registry, all with the same builder. You could say this whole post is about the chain those registries form as parents and children.

Example 1: printing the parent chain from a project registry

I checked from a build script that they really are connected this way. The project's registry is available through the internal API ProjectInternal.getServices(). And the private field allServices of DefaultServiceRegistry has a toString() that prints the whole parent chain (section 3 shows why).

// An example written for this post (not Gradle source code)
import org.gradle.api.internal.GradleInternal
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.internal.service.DefaultServiceRegistry
// ...

println("project class        = ${project.javaClass.name}")
val projectServices = (project as ProjectInternal).services
println("project registry     = $projectServices (${projectServices.javaClass.simpleName})")

// toString() of DefaultServiceRegistry's private field allServices shows the whole parent chain
val allServices = DefaultServiceRegistry::class.java.getDeclaredField("allServices").apply { isAccessible = true }
println("lookup chain         = ${allServices.get(projectServices)}")

[Shortened from the example ex/u04-injection/project/build.gradle.kts]

org.gradle.internal and ProjectInternal are Gradle's internal API. They are used here to read the source; don't use them in a real build. This example was run in a Docker container (Eclipse Temurin JDK 25) with a Gradle 9.10.0 development build compiled straight from the reference commit (07ecbf9ead). Here is the start of gradle probe --offline --console=plain.

=== gradle probe ===

Welcome to Gradle 9.10.0-20261107025045+0000!

Starting a Gradle Daemon (subsequent builds will be faster)
Shared read-only dependency cache is an incubating feature.

> Configure project :
project class        = org.gradle.api.internal.project.DefaultProject_Decorated
project registry     = project services (ScopedServiceRegistry)
lookup chain         = [project services, [Build-scoped services, [build tree services, [build session services, [[services for Gradle user home dir /root/.gradle, [Global services, [logging services, native services]]], [cross session services, [Global services, [logging services, native services]]]]]]]]

The Shared read-only dependency cache … line appears because this series' image mounts pre-downloaded libraries as a read-only cache (part 1). The line to read is the last one. Each pair of brackets means "own services, then the parents". Drawn out, it looks like this.

The parent chain of service registries. ① project services (Project scope) has ② Build-scoped services as its parent, whose parent is ③ build tree services, whose parent is ④ build session services. build session services has two parents: first ⑤ services for Gradle user home dir (UserHome), second ⑦ cross session services (CrossBuildSession). Both have ⑥ Global services as their parent, and Global services has two parents, logging services and native services. The numbers give the lookup order. Yellow boxes are registries that stay in the daemon after the build ends.

Why split it into many boxes?

As we saw in part 1, the Gradle daemon is a long-lived process that takes one build after another. So objects have different lifetimes. Some, like the file hash cache, must survive the end of a build so the next build is faster; others, like this build's project model, must be thrown away when the build ends. With a separate registry per scope, "the build is over" becomes "close the Build registry", and the services inside are cleaned up together. Children can use their parents' services, but parents don't know their children, so the structure itself prevents a long-lived service from holding on to an object that is about to go away.

The doc comment on @ServiceScope states this rule directly: "Services are visible to other services in the same scope and descendent scopes. Services are not visible to services in ancestor scopes" (ServiceScope.java lines 26–38).

Easy to get wrong

2. Declaring services — providers and @Provides

The standard way to put services into a registry is a provider class. ServiceRegistrationProvider is a marker interface with no methods; the rules live in its doc comment (ServiceRegistrationProvider.java lines 19–139). In short:

Method shape Meaning
@Provides + name starts with create… or decorate… + has a return type a factory for a service of the return type. Parameters are the services it depends on
same as above, but one parameter has the same type as the return type a decorator — that parameter receives the parent registry's service of the same type
named configure + void + a ServiceRegistration parameter registers in code, adding services with registration.add(...)
a parameter of type List<T> receives every T in this registry and its parents
a parameter of type ServiceRegistry receives the registry that owns this service

The class that actually enforces these rules is RelevantMethods. It goes through a provider class's methods and sorts them into three groups.

        private void addMethod(Method method) {
            if (method.getName().equals("configure")) {
                if (!method.getReturnType().equals(Void.TYPE)) {
                    throw new ServiceValidationException(String.format("Method %s.%s() must return void.", type.getName(), method.getName()));
                }
                add(configurers, method);
            } else if (method.getName().startsWith("create") || method.getName().startsWith("decorate")) {
                if (method.getAnnotation(Provides.class) == null) {
                    throw new ServiceValidationException(String.format("Method %s.%s() must be annotated with @Provides.", type.getName(), method.getName()));
                }
                if (method.getReturnType().equals(Void.TYPE)) {
                    throw new ServiceValidationException(String.format("Method %s.%s() must not return void.", type.getName(), method.getName()));
                }
                if (takesReturnTypeAsParameter(method)) {
                    add(decorators, method);
                } else {
                    add(factories, method);
                }
            } else if (method.getAnnotation(Provides.class) != null) {
                throw new ServiceValidationException(String.format("Non-factory method %s.%s() must not be annotated with @Provides.", type.getName(), method.getName()));
            }
        }

platforms/core-runtime/service-registry-impl/src/main/java/org/gradle/internal/service/RelevantMethods.java lines 75–96

If the name and the annotation disagree, it throws right away: a method starting with create without @Provides fails, and so does @Provides on a method whose name doesn't start with create/decorate. The result is remembered per class in a ClassValue (lines 30–35), so building many registries from the same provider class costs reflection only once. Since a new registry is built with ProjectScopeServices for every project, this cache matters.

DefaultServiceRegistry.addProvider() takes the sorted methods and turns them into service objects.

    private void findProviderMethods(ServiceRegistrationProvider target, ServiceAccessToken token) {
        Class<? extends ServiceRegistrationProvider> type = target.getClass();
        RelevantMethods methods = RelevantMethods.getMethods(type);
        for (ServiceMethod method : methods.decorators) {
            if (parentServices == null) {
                throw new ServiceLookupException(String.format("Cannot use decorator method %s.%s() when no parent registry is provided.", type.getSimpleName(), method.getName()));
            }
            ownServices.add(new FactoryMethodService(this, determineAccessScope(method, token), token, target, method));
        }
        for (ServiceMethod method : methods.factories) {
            ownServices.add(new FactoryMethodService(this, determineAccessScope(method, token), token, target, method));
        }
        for (ServiceMethod method : methods.configurers) {
            applyConfigureMethod(token, method, target);
        }
    }

platforms/core-runtime/service-registry-impl/src/main/java/org/gradle/internal/service/DefaultServiceRegistry.java lines 172–187

Two things are worth noticing.

The registration.add(...) used inside configure comes in two forms. add(type, object), which takes an object that already exists, becomes a FixedInstanceService; add(type, implementationClass), which takes a class, becomes a ConstructorService that calls the class's constructor later (lines 235–264). Constructor parameters are looked up as services, just like @Provides method parameters. So in the end each service is wrapped as one of three kinds.

The kinds of service inside a registry. ManagedObjectServiceProvider implements the ServiceProvider interface (getService, getAll). It creates the object the first time getInstance() is called, and stop() closes the services that use it first. Below it, SingletonService moves through the UNBOUND, BINDING and BOUND states in prepare(). SingletonService's subclasses are FixedInstanceService, which holds an existing object, and FactoryService, which finds parameter services in bind() and creates the object in createServiceInstance(). FactoryService's subclasses are FactoryMethodService, which calls a @Provides method, and ConstructorService, which calls a constructor. Except for the ServiceProvider interface, all of these are private nested classes inside DefaultServiceRegistry.

Method handle or reflection?

Two ways of calling @Provides methods are prepared. DefaultServiceMethodFactory picks the method handle (java.lang.invoke.MethodHandle) factory if method handles are available, otherwise the reflection factory (DefaultServiceMethodFactory.java lines 25–36). But choosing the method handle factory doesn't mean everything is called through a method handle.

    @Override
    public ServiceMethod toServiceMethod(Method method) {
        if (Modifier.isPublic(method.getModifiers()) && Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
            try {
                return new MethodHandleBasedServiceMethod(method);
            } catch (IllegalAccessException ex) {
                return new ReflectionBasedServiceMethod(method);
            }
        }
        return new ReflectionBasedServiceMethod(method);
    }

platforms/core-runtime/service-registry-impl/src/main/java/org/gradle/internal/service/MethodHandleBasedServiceMethodFactory.java lines 34–44

A method handle is used only when both the method and its class are public (because it uses MethodHandles.publicLookup()). Yet most of Gradle's provider methods are protected or have no access modifier. Counting the methods annotated with Gradle's @Provides (org.gradle.internal.service.Provides) in the Java sources of the reference commit gave 648: 22 public, 168 protected, and the other 458 package-private (two example files that use the same-named @Provides of Guice and Dagger were left out). So most are called through reflection. Each such method is called once per service, so the speed difference probably doesn't matter much (I haven't measured it).

Easy to get wrong: the parameters, not the name, make a decorator

You might think a decorator has to start with decorate, but all RelevantMethods checks is "does a parameter have the same type as the return type?" (lines 88–92 of the code above). So there are also decorators named create…. The task factory in the project registry is one.

    @Provides
    protected ITaskFactory createTaskFactory(ServiceRegistry projectScopeServiceRegistry, ITaskFactory parentFactory, TaskScheme taskScheme) {
        return parentFactory.createChild(project, taskScheme.getInstantiationScheme().withServices(projectScopeServiceRegistry));
    }

subprojects/core/src/main/java/org/gradle/internal/service/scopes/ProjectScopeServices.java lines 251–254

ITaskFactory parentFactory has the return type, so this is a decorator. That parameter receives the Build registry's ITaskFactory (the AnnotationProcessingTaskFactory created in BuildScopeServices lines 490–496), and the project registry offers a child factory with "this project and this project's services" attached. That's why ITaskFactory declares two scopes with @ServiceScope({Scope.Build.class, Scope.Project.class}). createPluginRegistry(PluginRegistry rootRegistry), earlier in the same file, is the same kind of decorator (lines 158–168). In section 6's example we'll see this factory create a task, in a stack trace.

3. Lookup and creation — lazy singletons

Own services first, then the parents

The registry's constructor fixes the lookup order.

    public DefaultServiceRegistry(@Nullable String displayName, ServiceRegistry... parents) {
        assertAllowedImplementation(getClass());

        this.displayName = displayName;
        this.ownServices = new OwnServices();
        if (parents.length == 0) {
            this.parentServices = null;
            this.allServices = ownServices;
            this.inspector = new ClassInspector();
        } else {
            this.parentServices = setupParentServices(parents);
            this.allServices = new CompositeServiceProvider(ownServices, parentServices);
            this.inspector = parents[0] instanceof DefaultServiceRegistry ? ((DefaultServiceRegistry) parents[0]).inspector : new ClassInspector();
        }
        this.thisAsServiceProvider = allServices;
    }

platforms/core-runtime/service-registry-impl/src/main/java/org/gradle/internal/service/DefaultServiceRegistry.java lines 106–121

allServices is CompositeServiceProvider(ownServices, parentServices). CompositeServiceProvider.getService() asks its members front to back and returns the first answer (lines 1058–1066). With several parents, parentServices is itself a CompositeServiceProvider of the parents in the order they were added (lines 123–135). The nested brackets printed in example 1 are exactly the toString() of these objects (lines 1086–1088). This matches the order written in the provider doc comment: "own services of the current registry → all services of the first parent → all services of the second parent → …. The decorator declarations skip the own services, and start the lookup in the parents" (ServiceRegistrationProvider.java lines 102–116).

assertAllowedImplementation() prevents subclassing this class, except for ScopedServiceRegistry (lines 75–83). It looks like registries used to be subclassed to add services; now that path is closed with a message telling you to use ServiceRegistryBuilder.

Looking among own services

        @Override
        public @Nullable Service getService(Type type, @Nullable ServiceAccessToken token) {
            PersistentArray<ServiceProvider> serviceProviders = getProviders(unwrap(type));
            if (serviceProviders.isEmpty()) {
                return null;
            }
            if (serviceProviders.size() == 1) {
                return serviceProviders.get(0).getService(type, token);
            }

            List<Service> services = new ArrayList<Service>(serviceProviders.size());
            for (ServiceProvider serviceProvider : serviceProviders) {
                Service service = serviceProvider.getService(type, token);
                if (service != null) {
                    services.add(service);
                }
            }
            // ...
            Formatter formatter = new Formatter();
            formatter.format("Multiple services of type %s available in %s:", format(type), getDisplayName());
            for (String description : descriptions) {
                formatter.format("%n   - %s", description);
            }
            throw new ServiceLookupException(formatter.toString());
        }

platforms/core-runtime/service-registry-impl/src/main/java/org/gradle/internal/service/DefaultServiceRegistry.java lines 412–449

getProviders() takes candidates from providersByType, a map keyed by type. When a service is registered, this map is filled with the declared type and all its supertypes (except Object) as keys (collectProvidersForClassHierarchyOf(), lines 1466–1493). So a service registered as FileSystemOperations is also found when you ask for one of its superinterfaces. Then: zero candidates gives null (and the lookup moves to the parents), one gives that candidate, and two or more throw. Unlike Spring, there is no @Primary or name-based mechanism for picking one (compare with Spring part 5).

The same map blocks one more thing. If ServiceRegistry shows up while walking the supertypes, it throws (lines 1477–1480). That's because ServiceRegistry is a special service: every registry already contains itself under that type (the OwnServices constructor, lines 400–410). This is what section 2's table meant by "a ServiceRegistry parameter receives the owning registry".

The order for looking up one service. If the parameter type is List, it walks own and all parent services, collecting every T and creating them right there. Otherwise it looks in own services' providersByType map (keyed by the type and its supertypes). One found: return it. Two or more: throw ServiceLookupException (Multiple services of type ...). Zero: look in all services of the first parent, then the second parent. If nothing is found anywhere, find returns null and get throws UnknownServiceException. A decorator method's same-type parameter skips own services and looks only in the parents. The lookup only moves to the parents when own services have zero matches. With two, it fails without looking at the parents.

Created on first lookup

Finding a service doesn't create it immediately. A service object goes through two steps. First prepare() does the "get ready to create" step (bind),

        private Service prepare() {
            if (state == BindState.BOUND) {
                return this;
            }
            synchronized (this) {
                if (state == BindState.BINDING) {
                    throw new ServiceValidationException("Cycle in dependencies of " + getDisplayName() + " detected");
                }
                if (state == BindState.UNBOUND) {
                    state = BindState.BINDING;
                    try {
                        bind();
                        state = BindState.BOUND;
                    } catch (RuntimeException e) {
                        state = BindState.UNBOUND;
                        throw e;
                    }
                }
                return this;
            }
        }

platforms/core-runtime/service-registry-impl/src/main/java/org/gradle/internal/service/DefaultServiceRegistry.java lines 680–700

and then, when someone asks for the object, getInstance() creates it exactly once with double-checked locking (lines 592–603).

FactoryService.bind() only locates a service for each parameter (lines 747–800). If the parameter type equals the return type (a decorator), it looks only in owner.parentServices; otherwise in owner.allServices. If nothing is found, it fails with a message like Cannot create service of type ... using ... as required service of type ... for parameter #N is not available. Only when createServiceInstance() runs does it call get() on each located parameter service in turn (which creates them one after another) and then call the method or constructor (lines 803–825). The created object stays in the instance field, so within one registry a service is a singleton.

One line of the doc comment gives the reason for this design: "Services are created lazily and might not be instantiated at all during the lifetime of the owning service registry" (ServiceRegistrationProvider.java line 134). BuildScopeServices alone has 58 @Provides, and the reference commit has 690 in total (lines starting with @Provides, counted with git grep over Java and Kotlin src/main, leaving out 34 lines that use the same-named @Provides of Guice and Dagger). A single build doesn't use all of them. Spring's ApplicationContext creates its singletons up front with preInstantiateSingletons() on startup (Spring part 4); Gradle's registry has no such step.

Example 2: using the registry directly, outside a build

The registry is plain Java code in a few jars of the Gradle distribution. So it can be used directly with javac/java, no build needed. I put the image's /opt/gradle/lib/* on the classpath and wrote a small program that imitates two scopes (Global and Build).

// An example written for this post (not Gradle source code)
// ... imports omitted ...
public class RegistryLab {

    // ---- service types: @ServiceScope says which scope each lives in ----
    @ServiceScope(Scope.Global.class)
    public interface Clock {
    // ... String now() ...
    @ServiceScope({Scope.Global.class, Scope.Build.class})
    public interface Greeter {
    // ... String greet(String name) ...
    @ServiceScope({Scope.Global.class, Scope.Build.class})
    public interface Plugin {
    // ... String id() ...
    @ServiceScope(Scope.Build.class)
    public interface Report extends Closeable {
    // ... String render() ...
    // ... also omitted: interface Unscoped (no @ServiceScope) and the log() helper ...

    // ---- a provider that adds services to the Global scope ----
    static class GlobalServices implements ServiceRegistrationProvider {
        @Provides
        Clock createClock() {
            log("create Clock");
            return () -> "12:00";
        }

        @Provides
        Greeter createGreeter(Clock clock) {
        // ... a Greeter returning "Hello, <name> at 12:00" ...
        @Provides
        Plugin createCorePlugin() {
        // ... a Plugin whose id is "core" ...
    }

    // ---- a provider that adds services to the Build scope ----
    static class BuildServices implements ServiceRegistrationProvider {
        // parameter type == return type makes it a "decorator" — it wraps the parent's Greeter
        @Provides
        Greeter decorateGreeter(Greeter parent) {
        // ... a Greeter that upper-cases the parent's result ...
        @Provides
        Plugin createJavaPlugin() {
        // ... a Plugin whose id is "java" ...

        // List<T> collects every T in this registry and its parents
        // ServiceRegistry is the registry that owns this service
        @Provides
        Report createReport(Greeter greeter, List<Plugin> plugins, ServiceRegistry owner) {
        // ... render() builds one line from greeter and plugins; close() logs ...
    }

    // ... main() builds two registries with the same builder as in section 1:
    // ...   global = scopeStrictly(Global) + GlobalServices
    // ...   build  = scopeStrictly(Build)  + BuildServices + parent(global)
    // ... then runs steps 1 to 8 of the output below ...
}

[Shortened from the example ex/u04-registry-lab/RegistryLab.java — the omitted method bodies also log the moment of creation with log("create …") on their first line; the real file's comments are in Korean]

ex/u04-registry-lab/run.sh runs javac -cp "/opt/gradle/lib/*" RegistryLab.java and then java. Here is the output of steps 1 to 3.

=== 1. build two registries (nothing is created yet) ===
built: lab build
=== 2. first lookup of Report in the build registry ===
    [create Plugin java]
    [create Plugin core]
    [create Clock]
    [create Greeter (global)]
    [decorate Greeter]
    [create Report, owner = lab build]
render(): HELLO, GRADLE AT 12:00 / plugins: java core
same instance next time? true
=== 3. the same type seen from each registry ===
build.get(Greeter)  -> HELLO, X AT 12:00
global.get(Greeter) -> Hello, x at 12:00
global.find(Report) -> null
build.getAll(Plugin) size = 2

The flow of one get(Report). ① The caller calls get(Report), which calls prepare(). ② In the bind step the Report service only locates Greeter. ③ A List is get() right away during bind, so Plugin java and core are created now. ④ Then getInstance() calls createServiceInstance(); ⑤ get() on the Greeter decorator parameter leads to ⑥ the decorator calling get() on the parent's Greeter, and ⑦ that calling get() on Clock. Clock, the global Greeter and the decorator are created in that order, and ⑧ finally Report is created and remembered. The bracketed lines are the log lines from the example output. Because locating (bind) and creating are separate steps, the Plugins were created first.

Easy to get wrong

Closing

    @Override
    public void close() {
        noLongerMutable();
        if (state.compareAndSet(State.STARTED, State.CLOSED)) {
            CompositeStoppable.stoppable(allServices).stop();
        }
    }

platforms/core-runtime/service-registry-impl/src/main/java/org/gradle/internal/service/DefaultServiceRegistry.java lines 299–305

allServices contains the parents too, but they are wrapped in ParentServices, whose stop() does nothing (lines 1091–1119, with the comment "Wraps a parent to ignore stop requests"). So only own services are closed. When one service is closed, the services in the same registry that use it (recorded with requiredBy() during bind) are closed first, then the service itself (lines 622–632). That's what the class comment means by "Instances are closed in reverse dependency order" (line 58). If the object is Closeable or Stoppable, close()/stop() is called; a service that was never created has nothing to close. The last step of the example:

=== 8. close the build registry, then the global one ===
    [close Report]
build closed; global still works: Hello, y at 12:00
IllegalStateException: lab global has been closed.

This is how the Build registry is closed when a build ends, and the Global registry when the daemon ends.

4. Scope checks — is every service in its place?

Even with one registry per scope, if a developer accidentally registered a build-lifetime service in the Global registry, it would survive the end of the build and carry the previous build's state around. What prevents this is the @ServiceScope annotation on service types and the ServiceScopeValidator that checks it.

The .scopeStrictly(Scope.Global.class) in section 1's builder code is the switch. When given a scope, the builder creates a ScopedServiceRegistry instead of a DefaultServiceRegistry (ServiceRegistryBuilder.java lines 169–180). All ScopedServiceRegistry does is one line in its constructor: add(new ServiceScopeValidator(scope, strict)) (ScopedServiceRegistry.java lines 41–43). The validator is itself a service. It is a special kind of service, an AnnotatedServiceLifecycleHandler, so the registry notifies it of every service registered after it (OwnServices.add(), lines 470–477). The validator then checks each declared type like this.

    private void validateScope(Class<?> serviceType) {
        if (ServiceScopeValidator.class.isAssignableFrom(serviceType)) {
            return;
        }

        if (ServiceScopeValidatorWorkarounds.shouldSuppressValidation(serviceType)) {
            return;
        }

        Class<? extends Scope>[] serviceScopes = scopeOf(serviceType);

        if (serviceScopes == null) {
            if (strict) {
                failWithMissingScope(serviceType);
            }
            return;
        }

        if (serviceScopes.length == 0) {
            throw new IllegalArgumentException(String.format("Service '%s' is declared with empty scope list", serviceType.getName()));
        }

        if (!contains(serviceScopes, scope)) {
            throw new IllegalArgumentException(invalidScopeMessage(serviceType, serviceScopes));
        }
    }

platforms/core-runtime/service-registry-impl/src/main/java/org/gradle/internal/service/ServiceScopeValidator.java lines 70–95

There are two rules. If the type has @ServiceScope, its list must include this registry's scope; and if the registry is strict (= scopeStrictly), having no @ServiceScope at all is also a failure. Every per-scope registry Gradle creates, from Global to Project, is built with scopeStrictly (git grep finds 10 places in src/main that call scopeStrictly(, and none that call the lenient scope(Scope…)). The exception is a registry built with no scope at all. The plugin validation helper PropertyValidationAccess builds its own registry named "Global services" without a scope (PropertyValidationAccess.java line 58), and no validator runs in a registry like that.

Steps 5 and 6 of example 2 break these two rules on purpose. Step 5 puts Clock, which is @ServiceScope(Scope.Global.class), into a Build registry; step 6 puts Unscoped, which has no annotation, into a strict Build registry.

=== 5. a Global-only service registered in a Build registry ===
IllegalArgumentException: The service 'RegistryLab$Clock' declares service scope 'Global' but is registered in the 'Build' scope. Either update the '@ServiceScope()' annotation on 'Clock' to include the 'Build' scope or move the service registration to one of the declared scopes.
=== 6. a service without @ServiceScope in a strict registry ===
IllegalArgumentException: The service 'RegistryLab$Unscoped' is registered in the 'Build' scope but does not declare it. Add the '@ServiceScope()' annotation on 'Unscoped' with the 'Build' scope.

Both failed in build(), unlike step 7's ambiguity error, which appeared at lookup. The check runs at registration time, so a wrong scope shows up as soon as the daemon builds the registry, even if the service is never looked up. It's designed to fail fast during development.

The decision order of ServiceScopeValidator.validateScope(). When a service is registered, the ServiceScopeValidator inside the ScopedServiceRegistry is notified and runs validateScope() for each declared type. If the type is the validator itself or is in the ServiceScopeValidatorWorkarounds list, it passes. Otherwise it checks whether the type has @ServiceScope. If not: in a strict registry (built with scopeStrictly) an IllegalArgumentException "is registered in the 'Build' scope but does not declare it" is thrown (example 2, step 6, Unscoped); in a non-strict registry it passes. If it does: an empty scope list throws "declared with empty scope list"; if the list contains this registry's scope it passes; otherwise it throws "declares service scope 'Global' but is registered in the 'Build' scope" (example 2, step 5, Clock). The validateScope() code above as a set of forks. The messages in the red boxes are example 2's actual output (we did not trigger the empty-list error in the example).

One type in several scopes — ObjectFactory

Some types list several scopes in @ServiceScope. ObjectFactory, which you use as objects in build scripts, is one.

@ServiceScope({Scope.Global.class, Scope.BuildTree.class, Scope.Project.class})
public interface ObjectFactory {

subprojects/core-api/src/main/java/org/gradle/api/model/ObjectFactory.java lines 52–53

There really is one factory method in each of the three scopes (GlobalScopeServices.java lines 263–279, BuildTreeScopeServices.java lines 159–176, WorkerSharedProjectScopeServices.java lines 169–172). All three build a DefaultObjectFactory with instantiatorFactory.decorate(<that scope's registry>). Global and BuildTree do it directly inside the method; Project hands its registry to an ObjectFactoryFactory, which does it (DefaultObjectFactoryFactory.java lines 57–69). The difference is the registry in the parentheses. It decides which registry's services get injected into objects created with objects.newInstance(...). Objects created with a project's objects must be able to receive project services, so each project needs its own ObjectFactory. The @ServiceScope doc comment puts it this way: "The additional instances of the service in lower scopes 'override' the instance from the parent for their scope and its children" (ServiceScope.java lines 33–35).

I compared the two in example 1's build script.

val buildServices = (gradle as GradleInternal).services
println("gradle.services      = $buildServices")
val fromProject = projectServices.get(ObjectFactory::class.java)
val fromBuild = buildServices.get(ObjectFactory::class.java)
println("project.objects === project registry's ObjectFactory? ${project.objects === fromProject}")
println("project registry's ObjectFactory === build registry's? ${fromProject === fromBuild}")
gradle.services      = Build-scoped services
project.objects === project registry's ObjectFactory? true
project registry's ObjectFactory === build registry's? false

project.objects is the project registry's, and it's a different object from the one found in the Build registry (gradle.services). The Build scope has no ObjectFactory, so the Build registry presumably climbed to its parent and returned the BuildTree one (this last part is inferred from the lookup order in the source; I didn't separately print whether the object is BuildTree's).

Easy to get wrong

5. How modules add services — GradleModuleServices

As part 1 showed, the Gradle repository is split into platforms and modules. Modules such as the configuration cache (configuration-cache), the build cache and dependency management also need to put services into each scope, but if BuildScopeServices in subprojects/core knew all of them, the dependency direction would be backwards. So there's a way for modules to register themselves. The interface GradleModuleServices has one method per scope, eight in all, from registerGlobalServices() to registerProjectServices() (GradleModuleServices.java lines 29–94). A module implements this interface (usually by extending the empty implementation AbstractGradleModuleServices and overriding only what it needs) and writes the class name into its jar's META-INF/services/org.gradle.internal.service.scopes.GradleModuleServices file. The file in subprojects/core has five lines (CoreServices, BuildCacheServices, ExecutionServices, VirtualFileSystemServices, BuildEventServices).

These files are read when the Global registry is built.

public class BuildProcessScopeServices implements ServiceRegistrationProvider {
    void configure(ServiceRegistration registration, ClassLoaderRegistry classLoaderRegistry) {
        List<GradleModuleServices> servicesProviders = new DefaultServiceLocator(classLoaderRegistry.getRuntimeClassLoader(), classLoaderRegistry.getPluginsClassLoader()).getAll(GradleModuleServices.class);
        for (GradleModuleServices services : servicesProviders) {
            registration.add(GradleModuleServices.class, services);
            services.registerGlobalServices(registration);
        }
    }

platforms/core-runtime/build-state/src/main/java/org/gradle/internal/buildprocess/BuildProcessScopeServices.java lines 37–44

It creates the module objects, registers them as Global services (registration.add(GradleModuleServices.class, services)) and has them register their Global share. Later, when the registry of another scope is built, these objects are taken back out of Global and asked to register their share for that scope. UserHome gets them with globalServices.getAll(GradleModuleServices.class) (GradleUserHomeScopeServices.java lines 126–128), and Project takes a List<GradleModuleServices> parameter in configure() and gets them through the collection injection from section 2 (ProjectScopeServices.java lines 141–156). The other scopes follow the same pattern (git grep "register…Services(registration)" shows a call site for each of the eight scopes).

How modules add services. DefaultServiceLocator.getAll() reads the META-INF/services/...GradleModuleServices files in 57 jars and turns the classes into objects. BuildProcessScopeServices.configure(), which builds the Global registry, also registers these GradleModuleServices objects as Global services and calls registerGlobalServices(). Later, when the UserHome registry is built, they are fetched again via getAll or List injection and registerGradleUserHomeServices() is called; when the BuildSession·CrossBuildSession, BuildTree·Build·Settings and Project registries are built, each scope's register method is called. The daemon holds one set of module objects; each time a new registry is created they are called on to add their share.

Example 3: counting the registered modules in the distribution

At the end of ex/u04-injection/run.sh, I went through every jar in the distribution and counted the jars containing this file and the classes listed in it.

=== GradleModuleServices listed in the distribution's jars ===
jars with the file: 57, implementation classes listed: 68
org.gradle.internal.service.scopes.CoreServices
org.gradle.caching.internal.BuildCacheServices
org.gradle.internal.service.scopes.ExecutionServices
org.gradle.internal.service.scopes.VirtualFileSystemServices
org.gradle.internal.build.event.BuildEventServices

The last five lines are the file's contents in gradle-core-9.10.0.jar. Counting on the source side, there are 57 such files under src/main/resources, with 68 class names once blank lines are dropped. The platforms with the most files are software (14), core-configuration (12), jvm (7) and core-runtime (6). The numbers match the distribution.

Easy to get wrong

6. All the way to user code — @Inject and _Decorated classes

So far it's been about Gradle's internal objects. Now let's follow how a task you write receives services. The public API offers two ways to get services into a task: put @Inject on the constructor, or on an abstract getter. I added a task that does both to example 1's build script.

// An example written for this post (not Gradle source code)
// A task that uses both constructor injection and getter injection
abstract class Probe @Inject constructor(
    private val fs: FileSystemOperations       // constructor parameter — looked up from services and passed in
) : DefaultTask() {

    @get:Inject
    abstract val objects: ObjectFactory        // abstract getter — implemented by the generated subclass

    init {
        // who creates this object? print only the org.gradle and build script frames
        println("Probe constructor, called through:")
        Throwable().stackTrace
            .filter { it.className.startsWith("org.gradle") || it.className.startsWith("Build_gradle") }
            .take(16)
            .forEach { println("    at ${it.className}.${it.methodName}:${it.lineNumber}") }
    }

    @TaskAction
    fun run() {
        println("task class  = ${javaClass.name}")
        println("superclass  = ${javaClass.superclass.name}")
        println("fs          = ${fs.javaClass.name}")
        println("objects     = ${objects.javaClass.name}")
        // fields and methods the generated subclass declares itself
        println("generated fields  = " + javaClass.declaredFields.map { it.name }.sorted())
        // ... also prints the method list, omitted here because it's long ...
    }
}

// ... the Greeting class created with objects.newInstance() is omitted (we'll see it later in section 6) ...

tasks.register<Probe>("probe")

[Shortened from the example ex/u04-injection/project/build.gradle.kts]

Here is the rest of the gradle probe output.

Probe constructor, called through:
    at Build_gradle$Probe.<init>:20
    at Build_gradle$Probe_Decorated.<init>:-1
    at org.gradle.internal.instantiation.generator.AsmBackedClassGenerator$InvokeConstructorStrategy.newInstance:2114
    at org.gradle.internal.instantiation.generator.AbstractClassGenerator$GeneratedClassImpl$GeneratedConstructorImpl.newInstance:592
    at org.gradle.internal.instantiation.generator.DependencyInjectingInstantiator.doCreate:64
    at org.gradle.internal.instantiation.generator.DependencyInjectingInstantiator.newInstanceWithDisplayName:50
    at org.gradle.api.internal.project.taskfactory.TaskFactory$1.call:88
    at org.gradle.api.internal.project.taskfactory.TaskFactory$1.call:82
    at org.gradle.internal.UncheckedException.uncheckedCall:103
    at org.gradle.api.internal.AbstractTask.injectIntoNewInstance:224
    at org.gradle.api.internal.project.taskfactory.TaskFactory.create:82
    at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory.create:47
    at org.gradle.api.internal.tasks.DefaultTaskContainer.createTask:362
    at org.gradle.api.internal.tasks.DefaultTaskContainer.access$200:77
    at org.gradle.api.internal.tasks.DefaultTaskContainer$TaskCreatingProvider.createDomainObject:760
    at org.gradle.api.internal.tasks.DefaultTaskContainer$TaskCreatingProvider.createDomainObject:717

> Task :probe
task class  = Build_gradle$Probe_Decorated
superclass  = Build_gradle$Probe
fs          = org.gradle.api.internal.file.DefaultFileSystemOperations_Decorated
objects     = org.gradle.api.internal.model.DefaultObjectFactory
generated fields  = [__objects__, _gr_dn_, _gr_map_, _gr_mc_, _gr_owner_]

BUILD SUCCESSFUL in 1m 9s

The innermost frame is at the top (cut at 16 frames; the long generated methods line was dropped from the output). Reading from the bottom: DefaultTaskContainer, which actually creates a task that was only registered (part 8 covers it) → the two task factories from section 2 (AnnotationProcessingTaskFactory, TaskFactory) → AbstractTask.injectIntoNewInstance → DependencyInjectingInstantiator → AsmBackedClassGenerator$InvokeConstructorStrategy → the constructor of Probe_Decorated → the constructor of my Probe. The duration differs from run to run (this run started a fresh daemon: 1 min 9 s).

The order in which a task is created and receives services. ① TaskFactory puts the project and task name in a thread local via AbstractTask.injectIntoNewInstance. ② It calls newInstanceWithDisplayName(Probe) on DependencyInjectingInstantiator. ③ The instantiator find()s the constructor parameter FileSystemOperations in the project registry and gets DefaultFileSystemOperations_Decorated. ④ It calls newInstance(services, ...) on InvokeConstructorStrategy, which ⑤ puts services in a thread local and ⑥ calls the Probe_Decorated constructor with fs. ⑦ The Probe constructor and the AbstractTask constructor run. Later, on the first read of objects in the task action, ⑧ the generated getter calls get(ObjectFactory) on the project registry and stores the result in a field. Constructor injection happens before the object exists; getter injection happens on first read.

Which registry? The project registry

Look again at section 2's createTaskFactory(): taskScheme.getInstantiationScheme().withServices(projectScopeServiceRegistry). It hands the project registry to the instantiator that creates tasks. So a task can receive any service in the chain printed in example 1, from the project through Build and on up to Global. What TaskScheme uses is instantiatorFactory.decorateScheme(), the "decorate and inject" mode (ExecutionGlobalServices.java lines 217–219).

Filling in constructor parameters

    private Object[] addServicesToParameters(Class<?> type, ClassGenerator.GeneratedConstructor<?> constructor, ServiceLookup services, Object[] parameters) {
        Class<?>[] parameterTypes = constructor.getParameterTypes();
        Type[] genericTypes = constructor.getGenericParameterTypes();
        Object[] resolvedParameters = new Object[parameterTypes.length];
        int pos = 0;
        for (int i = 0; i < resolvedParameters.length; i++) {
            Class<?> targetType = parameterTypes[i];
            Type serviceType = genericTypes[i];
            if (pos < parameters.length) {
                // ...
                if (parameter == null || targetType.isInstance(parameter)) {
                    resolvedParameters[i] = parameter;
                    pos++;
                    continue;
                }
            }
            Object service = services.find(serviceType);
            if (service != null) {
                resolvedParameters[i] = service;
                continue;
            }
            TreeFormatter formatter = new TreeFormatter();
            formatter.node("Unable to determine constructor argument #" + (i + 1) + ": ");
            // ...

platforms/core-configuration/model-core/src/main/java/org/gradle/internal/instantiation/generator/DependencyInjectingInstantiator.java lines 165–193

It walks the constructor parameters front to back. If an argument passed by the caller (the arguments of tasks.register<T>("name", args…)) has a matching type, it uses that; otherwise it looks the type up in the registry with services.find(type). That's why passed arguments and services can be mixed. If neither works, it fails. I added a task that takes a single String to the example and ran it.

// a constructor parameter that can't be injected (String is not a service)
abstract class Broken @Inject constructor(val label: String) : DefaultTask()
tasks.register<Broken>("broken")
=== gradle broken (expected to fail) ===
> Configure project :
* What went wrong:
> Could not create task ':broken'.
   > Could not create task of type 'Broken'.
      > Unable to determine constructor argument #1: missing parameter of type String, or no service of type String.

(Only the failure lines were kept, with grep; the > Run with … hint lines and the BUILD FAILED line that follow were cut.) It means "no such argument was passed, and there's no service of type String". The rules for choosing the constructor are in InjectUtil.selectConstructor(): if there's a single constructor with no parameters (and it is public or package-private), it's used as is; a constructor with parameters must carry @Inject; and with several constructors exactly one must carry @Inject (InjectUtil.java lines 36–87).

Two thread locals

The stack contained AbstractTask.injectIntoNewInstance and InvokeConstructorStrategy.newInstance. Both use the same technique. The first puts the task name and project into the thread local NEXT_INSTANCE, calls the factory, and clears it in finally (AbstractTask.java lines 221–228). The second looks like this.

        @Override
        public Object newInstance(ServiceLookup services, InstanceGenerator nested, @Nullable Describable displayName, Object[] params) throws InvocationTargetException, IllegalAccessException, InstantiationException {
            ObjectCreationDetails previous = SERVICES_FOR_NEXT_OBJECT.get();
            SERVICES_FOR_NEXT_OBJECT.set(new ObjectCreationDetails(nested, services, displayName, roleHandler));
            try {
                return constructor.newInstance(params);
            } finally {
                SERVICES_FOR_NEXT_OBJECT.set(previous);
            }
        }

platforms/core-configuration/model-core/src/main/java/org/gradle/internal/instantiation/generator/AsmBackedClassGenerator.java lines 2109–2118

Right before calling the constructor, it puts "what the object being created will need" (task name and project, the registry) into a thread local, and clears it afterwards. The constructors users write take no arguments, like DefaultTask(), so AbstractTask's constructor takes the project from the thread local (lines 172–178) and grabs the project registry from it with this.services = project.getServices() (line 198). The generated class likewise takes the registry from the thread local while it is being constructed (getServicesForNext(), lines 138–140). It's a way to pass hidden arguments without changing constructor signatures. When the thread local is empty, the AbstractTask constructor throws Task of type '…' has been instantiated directly which is not supported. (line 183). That's why you can't create a task with new.

What the generated class does

Probe_Decorated is what implements the abstract getter getObjects(). The comment AsmBackedClassGenerator leaves where it generates the bytecode shows the shape directly.

        @Override
        public void applyServiceInjectionToGetter(PropertyMetadata property, @Nullable final Class<? extends Annotation> annotation, MethodMetadata getter) {
            // GENERATE public <type> <getter>() { if (<field> == null) { <field> = <services>>.get(<service-type>>); } return <field> }

platforms/core-configuration/model-core/src/main/java/org/gradle/internal/instantiation/generator/AsmBackedClassGenerator.java lines 1093–1095

If the field is empty, it looks the service type up in the registry with get(serviceType) and stores it in the field; after that it returns the field. The field name is "__" + name + "__" (propFieldName(), lines 1952–1954). The __objects__ in the example's generated fields is that field.

So what is <services>? The two classes in the example give different answers. I also printed the fields of the object created in the build script with objects.newInstance(Greeting::class.java).

objects.newInstance  = Build_gradle$Greeting_Decorated (providers = DefaultProviderFactory_Decorated)
its generated fields = [__message__, _gr_dn_, _gr_dyn_, _gr_f_, _gr_map_, _gr_mc_, _gr_owner_, _gr_svcs_]

Greeting_Decorated has a _gr_svcs_ field holding the registry; Probe_Decorated doesn't. If the class already has a services property returning a ServiceRegistry, the generator uses it instead of creating its own field.

        @Override
        public boolean claimPropertyImplementation(PropertyMetadata property) {
            if (property.getName().equals("services") && property.isReadable() && ServiceRegistry.class.isAssignableFrom(property.getType())) {
                hasServicesProperty = true;
                return true;
            }
            return false;
        }

platforms/core-configuration/model-core/src/main/java/org/gradle/internal/instantiation/generator/AbstractClassGenerator.java lines 1414–1421

A task has AbstractTask.getServices() (line 696), so the getter calls this.getServices(), the project registry we just saw (putServiceRegistryOnStack(), lines 1332–1342). Greeting has no such property, so a _gr_svcs_ field and a $gradleServices() method are generated (lines 1763–1767). DefaultProject is also on the getServices() side.

The shape of the generated classes. AbstractTask returns the project registry from getServices(). The abstract class Probe, which inherits it via DefaultTask, has an abstract getter getObjects(); the generated Probe_Decorated adds an objects field and implements getObjects(), looking it up with getServices().get() if the field is empty. The abstract class Greeting has an abstract getter getMessage(); the generated Greeting_Decorated has a gr_svcs field (ServiceLookup) and a message field, and $gradleServices() returns the field, or the thread local if it's empty. The field names are the real names printed with declaredFields in the example.

That's also why the build script's project was a DefaultProject_Decorated. DefaultProject is an abstract class, and many of its getters behind project.repositories, project.configurations and project.objects are @Inject abstract (for example getArtifacts(), getRepositories() and getConfigurations(), DefaultProject.java lines 556–566). @Inject appears 29 times in DefaultProject.java. The first time you write repositories { … } in a build script, the generated getter fetches the RepositoryHandler from the project registry. Gradle's own core model uses exactly the same injection machinery as user tasks.

Why getters instead of constructors?

With constructor injection only, receiving one more service means changing the constructor, and every subclass constructor has to follow. An abstract getter just declares "I need this type" without touching the constructor, and the subclass Gradle generates fetches the value on first read. Services you never use are never even looked up. It's the same idea as section 3's lazy singletons, carried all the way to user objects. The technique resembles Spring creating a subclass with CGLIB to intercept methods (Spring part 10), but the purpose differs: Spring's proxy adds behaviour (transactions and so on) around methods that already exist, while Gradle's _Decorated fills in empty (abstract) methods.

Easy to get wrong

7. Compared with Spring's IoC container

Both are "DI containers", but they look quite different. Side by side, with the Gradle column checked in the source in this post and the Spring column from the Spring series:

Gradle service registry Spring BeanFactory/ApplicationContext
Hierarchy one registry per scope, possibly several parents a single parent factory (HierarchicalBeanFactory, part 3)
Declaration @Provides create…() / configure() in provider classes BeanDefinition (@Bean, component scanning, etc.)
Lookup key type only name and type
Two candidates always an error within that registry picks one by @Primary, @Qualifier, name, etc. (part 5)
When created on first lookup (lazy) non-lazy singletons up front on startup (part 4)
Circular dependencies detected via the BINDING state, then an error field and setter injection resolved with the three-level cache (part 4)
Lifetime checks @ServiceScope + ServiceScopeValidator, checked at registration each bean picks a scope; no such check at registration
User objects a _Decorated subclass fills in abstract getters a CGLIB subclass intercepts methods (part 10)

Gradle's side is simpler and stricter: one key (the type), failure on ambiguity, failure on a wrong scope. Unlike Spring, where users assemble the container themselves, only Gradle's developers register services in Gradle's registry and users just take them out; that difference seems to explain these choices (this is my interpretation, not something the source says).

Comparing the hierarchy shapes of the two containers. On the left, in Gradle's service registry, the BuildSession registry has two parents — the first parent, the UserHome registry, and the second parent, the CrossBuildSession registry — and both have the Global registry as their parent. Lookup uses only the type as the key, two candidates in one registry is an error, and a service is created the first time it is looked up. On the right, in Spring's BeanFactory and ApplicationContext, a child context has one parent factory (HierarchicalBeanFactory). getBean looks up by name or type, picks between two candidates using @Primary, @Qualifier or the name, and creates non-lazy singletons up front at startup. Arrows point to the parent. The Gradle side is just the BuildSession part of the parent chain picture in section 1.

What this post leaves out

Compared with the 9.8.2 release, the four registry modules of the reference commit (service-lookup, service-provider, service-registry-builder, service-registry-impl) are identical except for one removed @SuppressWarnings line in ReflectionBasedServiceMethod.java (checked with git diff v9.8.2 07ecbf9ead). So the registry itself is the same in the widely used 9.8.x as in this post. The class generator (instantiation/generator) has changed a little since, so the line numbers only match this commit.

Summary

Concept One-line summary
Service registry a box handing out services by type. One implementation, DefaultServiceRegistry, built by ServiceRegistryBuilder
Scopes and registries one registry per scope. Project → Build → BuildTree → BuildSession → (UserHome, CrossBuildSession) → Global
Lookup order own → all of the first parent → all of the second parent. Two among own services is an error, without looking at the parents
Providers and @Provides create…/decorate… methods are factories; a parameter of the return type makes a decorator; configure() runs immediately
Lazy singletons registration only wraps; bind on first lookup; created on first get(). Except List<T>, which is created during bind
Cycles and ambiguity cycles detected via the BINDING state and reported as errors; two of the same type fail at lookup
Closing only own services, users first. ParentServices shields the parents
@ServiceScope the scope a service type lives in. scopeStrictly registries check it at registration
GradleModuleServices 57 modules register through META-INF/services and add their share whenever a registry is created
@Inject constructor DependencyInjectingInstantiator uses passed arguments first, then looks up the project registry
_Decorated a subclass generated with ASM. It fills abstract getters with "on first read, get() and keep in a field"
Thread locals a way to pass the project and registry without changing constructor signatures

What's next

With this post, the foundation inside the daemon is in place. Part 3 showed how the daemon receives a build, and this post how the objects inside it find each other. Next comes the order in which a build actually runs on top of that. The executor chain after ExecuteBuild creates the build tree and the build, and DefaultBuildLifecycleController guards loading settings → configuring projects → the task graph → execution with state transitions. The registries in this post are exactly what those state objects (BuildSessionState, BuildTreeState, AbstractBuildState) create. It continues in part 5: The Build Lifecycle — Where Initialization, Configuration and Execution Split.

Earlier series covered similar ground. Spring part 3 read the parent-child hierarchy of BeanFactory, and Spring part 5 the rules for picking one among several candidates; Gradle is the side that has no such rules at all. The CGLIB proxies of Spring part 10 are the same "create a subclass at run time" as this post's _Decorated. Registries living separately per scope resemble class loaders living separately per web app (Tomcat part 4): both let you see your parent's things but not your siblings' or children's, so discarding one doesn't hurt the others.

More posts