namalzi

← All posts

Digging into Spring's Source 2: The Big Map — Modules and Packages

Spring · Published

A map for anyone opening the Spring Framework repository for the first time. What each of the 22 modules does, how they depend on one another, what lives inside spring-core, spring-beans, spring-context and spring-aop, and the big picture of container start-up — all checked against the real build files and source.

What this post covers

This is the second post in "Digging into Spring's Source". Part 1 covered what Spring is, why read its source, and in what order. This post is the big map you spread out before reading any code line by line.

Clone the Spring Framework repository and you get more than twenty folders that start with spring-. The names alone do not tell you which ones are the centre and which are the outskirts. By the end of this post you will know:

The goal: when a later post says "this class lives in spring-beans", you can place it on the map and know what that part of the map is for.

Item Detail
Source Spring Framework 7.1.0-SNAPSHOT
Commit f447f3c310 (2026-10-01, main branch)
How it was checked module list against settings.gradle, dependencies against each module's build file, file counts counted by hand in the checkout

Line numbers and details can differ in other versions. Part 1 explains how to get the source at this exact commit.

Five words to know first

A module is a bundle of code that is built and published on its own. Spring Framework is not one big lump; it is a collection of modules such as spring-core and spring-beans, and each module becomes one jar file (an archive of compiled Java classes). That lets an application pull in only the modules it needs.

A package is Java's folder for grouping classes inside a module, with a name like org.springframework.beans.factory. In this post the leading org.springframework. is dropped, so that package is written beans.factory.

A dependency means "uses". If code in module A uses a class from module B, then A depends on B, drawn as A → B. Without B, A does not compile.

Bean and container. An object that Spring creates and manages for you is a bean; the thing that creates beans, wires them together and keeps them is the container. When the container hands a bean the other beans it needs, that is dependency injection (DI). Part 1 explains these in more detail.

Proxy and AOP. A proxy is a stand-in object that wraps the real object and receives calls on its behalf. Because it gets the call first, it can do something before and after the real method runs. Using that trick to apply a shared concern — "begin and end a transaction", say — to many methods at once is called AOP (aspect-oriented programming).

1. What you see when you open the repository: 22 modules

Spring Framework is built with Gradle, a build tool (a program that compiles source and packages it into jars). The settings.gradle file at the top of the repository is the list of "these are the modules in this repository".

include "spring-aop"
include "spring-aspects"
include "spring-beans"
include "spring-context"
// ...
include "spring-webmvc"
include "spring-websocket"
include "framework-api"
include "framework-bom"
include "framework-docs"
include "framework-platform"
include "integration-tests"

rootProject.name = "spring"
rootProject.children.each {project ->
	project.buildFileName = "${project.name}.gradle"
}

settings.gradle, lines 5–36

Three things can be read from it.

  1. There are 22 lines that start with spring-. Those are Spring Framework itself, and they match the top-level folder names one to one.
  2. The other five (framework-api, framework-bom, framework-docs, framework-platform, integration-tests) are documentation, version lists and integration tests. You can skip them when reading how the framework works.
  3. Because of the last three lines, a module's build file is not the usual build.gradle but <module name>.gradle. The build file of the spring-context module, for example, is spring-context/spring-context.gradle. Section 3 opens that file.

Every module folder has the same shape. The real source is under src/main/java, as in spring-beans/src/main/java/org/springframework/beans/…, and the tests are under src/test. This series reads src/main/java.

The settings.gradle file at the top of the repository has 27 include lines. The 22 that start with spring- are Spring Framework itself. For example, the spring-beans folder holds the build file spring-beans.gradle (module name.gradle, not build.gradle), the real source under src/main/java/org/springframework/beans/… that this series reads, and the tests under src/test/…. The other 5 (framework-api, framework-bom, framework-docs, framework-platform, integration-tests) are docs, a version list and integration tests, and can be skipped The shape of the repository, branching out from the single settings.gradle file. The green box is what this series reads; gray boxes can be skipped.

2. Four layers on one page

There is no need to memorise 22 names at once. Start by sorting them into layers.

The main Spring Framework modules in four layers: web and data at the top, container and AOP in the middle, and spring-core as the foundation at the bottom, with arrows pointing downward. An arrow means "the upper layer uses the lower one". The middle layer with the thick border is where this series starts reading.

What matters is the direction. An upper layer knows the layer below it; a lower layer does not know the ones above. None of the four middle-layer modules uses a single module from the data or web layers. That is why the middle layer can be read on its own, and why — once you know it — an upper layer is mostly a matter of "what did they add on top?".

3. Where the dependencies are written down

The arrows in that picture are not a guess; they are what the build files say. Let's check.

api and optional in a build file

This is the part of the spring-context build file that points at other modules.

dependencies {
	api(project(":spring-aop"))
	api(project(":spring-beans"))
	api(project(":spring-core"))
	api(project(":spring-expression"))
	// ...
	optional(project(":spring-instrument"))
	// ...
	optional("jakarta.annotation:jakarta.annotation-api")
	// ...
}

spring-context/spring-context.gradle, lines 9–19

project(":spring-aop") points at the spring-aop module in the same repository. Two different words come in front of it.

Written as Meaning In this post's diagrams
api(...) Required. It also travels along to whoever uses spring-context solid line
optional(...) Used at compile time, but does not travel along to users. If it is there it is used; if not, only that feature is missing dotted line

optional is not a built-in Gradle word; Spring added it to its own build. It is defined in OptionalDependenciesPlugin.java, whose comment says the configuration "is part of the project's compile and runtime classpaths but does not affect the classpath of dependent projects".

The central axis: five modules

Unfolding the middle layer and the foundation into modules gives this.

spring-context depends on spring-aop and spring-expression; spring-aop depends on spring-beans; spring-beans and spring-expression depend on spring-core. A → B means "A uses B". All of these are api dependencies; arrows you can already reach by following other arrows are left out (spring-context, for example, also declares spring-beans and spring-core directly).

Read from the bottom up and you see the order in which things stack.

  1. spring-core is at the bottom. Its build file has no line pointing at another module.
  2. spring-beans uses only spring-core. The core of the container is here.
  3. spring-aop sits on top of spring-beans — to make a proxy you need a bean to wrap.
  4. spring-expression uses only spring-core. It stands on its own, unrelated to spring-beans.
  5. spring-context gathers all four. ApplicationContext, the thing people usually mean by "the Spring container", lives here.

The modules built on the axis

Drawing a few modules of the data and web layers the same way:

spring-webmvc and spring-webflux depend on spring-web; spring-orm depends on spring-jdbc, which depends on spring-tx; spring-web and spring-tx are connected to spring-beans with solid lines and to spring-context with dotted lines. Solid lines are api, dotted lines are optional. Every arrow drawn was confirmed in a build file; some are left out for readability (nearly every module declares spring-core directly, for instance).

Easy to get wrong: a dotted line means "can live without it"

It is natural to assume that using transactions obviously requires spring-context. The build file says otherwise.

dependencies {
	api(project(":spring-beans"))
	api(project(":spring-core"))
	optional(project(":spring-aop"))
	optional(project(":spring-context"))  // for JCA, @EnableTransactionManagement
	// ...
}

spring-tx/spring-tx.gradle, lines 5–9

The only things spring-tx strictly requires are spring-beans and spring-core. As the comment says, spring-context is there only for JCA and @EnableTransactionManagement. Because the modules are cut small and only the truly necessary ones are tied with solid lines, an application is not forced to carry modules it does not need.

So how does the code deal with something that may or may not be there? It asks first whether the class is present. The spring-context build file above had optional("jakarta.annotation:jakarta.annotation-api"); the code that uses it looks like this.

	private static final boolean JAKARTA_ANNOTATIONS_PRESENT =
			ClassUtils.isPresent("jakarta.annotation.PostConstruct", classLoader);

spring-context/…/context/annotation/AnnotationConfigUtils.java, lines 119–120

ClassUtils.isPresent answers "can a class with this name be loaded?" with true or false. When you meet a line like this while reading the source, think "this is where an optional dependency is handled" and move on.

4. All 22 modules, one line each

Here is the full list. The file counts are the .java files under each module's src/main/java, counted directly in the checkout (including package-info.java; tests and Kotlin sources excluded).

Module What it does Files
spring-core Reading type and annotation information, resources (files etc.), environment (configuration values), type conversion, utilities 738
spring-beans BeanFactory (the core of the container), BeanDefinition (a bean's blueprint), dependency injection 338
spring-aop AOP done with proxies 226
spring-expression SpEL, a small language that evaluates an expression written as a string at run time 123
spring-context ApplicationContext, annotation-based configuration such as @Configuration, @Bean, @Component, events, plus extras like caching, scheduling and validation 625
spring-context-support Bridges to external libraries (Caffeine and JCache caches, mail, Quartz, FreeMarker) 87
spring-tx Transactions and @Transactional, data-access exceptions 173
spring-jdbc JdbcTemplate and other tools that make JDBC easier 253
spring-orm JPA integration (including Hibernate) 65
spring-r2dbc Reactive database access 65
spring-web The shared base for working with HTTP. HTTP clients such as RestClient are here too 792
spring-webmvc The servlet-style web framework centred on DispatcherServlet (Spring MVC) 363
spring-webflux The reactive web framework centred on DispatcherHandler, and WebClient 276
spring-websocket WebSocket, SockJS 170
spring-messaging The Message abstraction, STOMP messaging 245
spring-jms JMS messaging (JmsTemplate) 122
spring-oxm Converting between objects and XML 28
spring-test Testing support (the TestContext framework, MockMvc and more) 460
spring-aspects Aspects written in AspectJ. Not the same thing as the proxy-based spring-aop 14
spring-instrument A single JVM agent class (InstrumentationSavingAgent) 2
spring-context-indexer A tool that builds a list of components at compile time. Marked deprecated since 6.1 13
spring-core-test Tools used by Spring's own tests 45

For "reactive", it is enough for now to read it as "handling requests asynchronously instead of holding one thread per request".

Added up, that is 5,223 files and about 840,000 lines (the line count is wc -l over the same files). The largest are spring-web (about 132,000 lines) and spring-core (about 128,000). spring-beans, the core of the container, is 338 files and about 64,000 lines — a modest share by volume, yet every other module stands on it.

A bar chart of the number of .java files under src/main/java in each of the 22 modules, largest first: spring-web 792, spring-core 738, spring-context 625, spring-test 460, spring-webmvc 363, spring-beans 338, spring-webflux 276, spring-jdbc 253, spring-messaging 245, spring-aop 226, spring-tx 173, spring-websocket 170, spring-expression 123, spring-jms 122, spring-context-support 87, spring-orm 65, spring-r2dbc 65, spring-core-test 45, spring-oxm 28, spring-aspects 14, spring-context-indexer 13, spring-instrument 2 The file counts from the table above, drawn as bars. spring-beans, the core of the container, comes sixth.

5. What is inside the four key modules

Let's go one level deeper into just the four modules the next posts keep visiting. There is no need to memorise class names now. A feel for "names like this live around here" is enough.

spring-core: the toolbox everyone uses

Package Typical classes What it does
core ResolvableType, MethodParameter, Ordered Type information including generics, ordering
core.annotation MergedAnnotations, AnnotationUtils, @AliasFor Reading annotations
core.io Resource, ResourceLoader Reading resources the same way whether they are files or on the classpath
core.env Environment, PropertySource, Profiles Configuration values and profiles
core.convert ConversionService Type conversion (string → number and so on)
util ClassUtils, ReflectionUtils, StringUtils, Assert Utilities (Part 16)
asm, cglib — External libraries carried along inside (explained below)

spring-beans: the core of the container

Package Typical classes What it does
beans.factory BeanFactory, ListableBeanFactory, FactoryBean, ObjectProvider The interfaces seen by code that uses the container
beans.factory.config BeanDefinition, BeanPostProcessor, BeanFactoryPostProcessor, Scope The bean blueprint, and the extension points for stepping into the container
beans.factory.support DefaultListableBeanFactory, AbstractAutowireCapableBeanFactory, DefaultSingletonBeanRegistry The implementation classes that do the actual work
beans.factory.annotation @Autowired, @Qualifier, @Value, AutowiredAnnotationBeanPostProcessor Injection driven by annotations
beans BeanWrapper, BeanUtils Reading and writing an object's properties
beans.factory.xml — Reading XML configuration files

Part 3 reads the first three rows (beans.factory, .config, .support); Part 4 reads how a bean gets created inside beans.factory.support.

spring-context: the container you actually use

Package Typical classes What it does
context ApplicationContext, ConfigurableApplicationContext, ApplicationEvent, Lifecycle The container's interfaces, events
context.support AbstractApplicationContext, GenericApplicationContext The container's implementation. The start-up sequence is here
context.annotation AnnotationConfigApplicationContext, @Configuration, @Bean, @ComponentScan, @Import, ConfigurationClassPostProcessor Configuration with annotations
stereotype @Component, @Service, @Repository, @Controller Markers that say "register this class as a bean"
context.event @EventListener, ApplicationEventMulticaster Delivering events
cache, scheduling, validation, resilience and others @Cacheable, @Scheduled, @Async, Validator, @Retryable Extra features built on top of the container

ApplicationContext is not a different thing that replaces BeanFactory; it is a BeanFactory with more added. Its declaration shows ApplicationContext extending ListableBeanFactory (ApplicationContext.java, lines 59–60). That is why spring-context sits above spring-beans in the layer picture of section 2.

Inside spring-context, AbstractApplicationContext and GenericApplicationContext in the context.support package implement the interfaces ApplicationContext and ConfigurableApplicationContext in the context package. Inside spring-beans, DefaultListableBeanFactory in beans.factory.support implements the interfaces BeanFactory and ListableBeanFactory in beans.factory. And because ApplicationContext extends ListableBeanFactory, spring-context stands on top of spring-beans Blue boxes are the packages that hold interfaces; green boxes are the support packages that hold their implementations. The implementation sometimes goes through an in-between interface (for example, DefaultListableBeanFactory implements ListableBeanFactory by way of ConfigurableListableBeanFactory).

spring-aop: proxies

Package Typical classes What it does
org.aopalliance Advice, MethodInterceptor, MethodInvocation Standard interfaces for intercepting a method call
aop Pointcut, Advisor, TargetSource The basic ideas: "where" (Pointcut) to insert "what" (Advice)
aop.framework ProxyFactory, JdkDynamicAopProxy, CglibAopProxy Where proxies are actually built
aop.framework.autoproxy AbstractAutoProxyCreator Where a proxy is put around a bean automatically as the bean is created
aop.aspectj — Support for AspectJ expressions and @Aspect

Easy to get wrong: names that do not match where things live

6. What happens when the container starts

Now that we know where the modules are, let's look at things in order of time. Here is about the smallest program that uses Spring.

// An example written for this post (not Spring's own code)
@Configuration
class AppConfig {
    @Bean
    GreetingService greetingService() {
        return new GreetingService();
    }
}

class GreetingService {}

public class Main {
    public static void main(String[] args) {
        var context = new AnnotationConfigApplicationContext(AppConfig.class);
        GreetingService service = context.getBean(GreetingService.class);
        context.close();
    }
}

@Configuration, @Bean and AnnotationConfigApplicationContext are all in spring-context's context.annotation package, and getBean is a method declared on BeanFactory in spring-beans. Placed on the map, this short program is using spring-context together with spring-beans beneath it.

The single line new AnnotationConfigApplicationContext(AppConfig.class) starts the container. Open the constructor and it is three lines.

	public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
		this();
		register(componentClasses);
		refresh();
	}

spring-context/…/context/annotation/AnnotationConfigApplicationContext.java, lines 89–93

It registers the classes it was given (register) and calls refresh(). Nearly everything about container start-up happens inside that refresh(). The method lists twelve steps in order, each with a comment; keeping only the lines this post needs, it looks like this.

	public void refresh() throws BeansException, IllegalStateException {
		// ...
			// Prepare this context for refreshing.
			prepareRefresh();
			// ...
				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);
				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);
				// ...
				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			// ...
	}

spring-context/…/context/support/AbstractApplicationContext.java, lines 582–662

Open the link and the whole method reads like a running order. You do not need all twelve steps yet; group them into four chunks (Part 7 walks through the twelve one by one).

The four stages of container start-up: collect definitions, process definitions, create beans (instantiate, inject, initialize), running — followed by shutdown. The big flow from the container starting to it shutting down. The three boxes inside stage 3 are how a single bean is made, repeated for every bean.

  1. Collect definitions. The container does not create objects straight away. First it gathers blueprints that say "which beans to make, and how". That blueprint is a BeanDefinition (Part 3 looks at it closely). In the example above, register registers the blueprint for AppConfig, and the @Bean method inside AppConfig is read and added as a blueprint during the invokeBeanFactoryPostProcessors step.
  2. Process definitions. Once the blueprints are in, and before any object is made, there is a chance to change or add blueprints. The thing that does this is a BeanFactoryPostProcessor; in refresh() it is invokeBeanFactoryPostProcessors. The very next line, registerBeanPostProcessors, gets the BeanPostProcessors ready for use in stage 3.
  3. Create beans. finishBeanFactoryInitialization actually builds the beans from the blueprints. As the source comment says, this covers singletons (beans the container makes once and shares) that are not lazy-init. One bean is made in three moves: instantiate (create the object) → inject (hand it the other beans it needs) → initialize (call its init methods and, if needed, wrap it in a proxy). Those three moves appear in order in doCreateBean of AbstractAutowireCapableBeanFactory under the names createBeanInstance, populateBean and initializeBean. That is the subject of Part 4.
  4. Running and shutdown. When finishRefresh publishes the "ready" event (ContextRefreshedEvent), start-up is over. Later, calling close() publishes a closing event and destroys the beans.

Easy to get wrong

7. Three pieces that many features repeat

The table in section 4 listed features such as transactions, caching and asynchronous execution. They look as if they were built separately, but open them up and they repeat the same three pieces.

The flow from @EnableXxx through @Import, a BeanPostProcessor and a proxy, ending with annotations such as @Transactional taking effect. The three blue boxes are the repeated pieces: register definitions, step into bean creation, intercept calls.

Let's follow transactions. Put @Transactional on a method and that method runs inside a transaction. The switch that turns the feature on is @EnableTransactionManagement.

Piece 1 — @Import registers bean definitions. This is the declaration of the switch annotation.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

spring-tx/…/transaction/annotation/EnableTransactionManagement.java, lines 159–163

@Import means "also register the bean definitions this class tells you about". Follow the selectImports method of the class it points at and, in the default mode (PROXY), it brings in two things. AutoProxyRegistrar registers the blueprint of InfrastructureAdvisorAutoProxyCreator, a BeanPostProcessor that puts proxies on automatically; ProxyTransactionManagementConfiguration registers as beans the things that do the real work of "begin and end a transaction", such as TransactionInterceptor. This all happens in stages 1 and 2 of section 6.

Piece 2 — a BeanPostProcessor steps into bean creation. A BeanPostProcessor is an extension point the container calls every time it makes a bean. AbstractAutoProxyCreator, the parent class of the InfrastructureAdvisorAutoProxyCreator just registered, has this method.

	public @Nullable Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (this.earlyBeanReferences.remove(cacheKey) != bean) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

spring-aop/…/aop/framework/autoproxy/AbstractAutoProxyCreator.java, lines 285–293

As the name says, it is called "after initialization" — the last box of stage 3 in the picture of section 6. It takes the bean, wraps it if needed (wrapIfNecessary), and otherwise returns it as it is.

Piece 3 — the proxy intercepts calls. The wrapped bean is stored in the container as a proxy. From then on, when someone calls a method on this bean, the proxy receives the call first, begins a transaction, calls the real method, and when it finishes commits or rolls back.

Other features take the same road. Every class name in the table below was confirmed in the source (for the default mode).

Feature Switch What @Import brings in The BeanPostProcessor that steps in Proxy
@Transactional @EnableTransactionManagement AutoProxyRegistrar, ProxyTransactionManagementConfiguration InfrastructureAdvisorAutoProxyCreator yes
@Cacheable @EnableCaching AutoProxyRegistrar, ProxyCachingConfiguration InfrastructureAdvisorAutoProxyCreator yes
@Async @EnableAsync ProxyAsyncConfiguration AsyncAnnotationBeanPostProcessor yes
@Scheduled @EnableScheduling SchedulingConfiguration ScheduledAnnotationBeanPostProcessor no

The last row is the one to notice. @Scheduled means "call this method at set times", so there is no call to intercept. ScheduledAnnotationBeanPostProcessor inspects the bean, registers the schedule, and returns the bean unchanged. Some features use all three pieces; some use only the first two.

That is why the middle layer is read first. Once you know how bean definitions get registered, when a BeanPostProcessor is called and how a proxy is made, most features in the upper layers can be read as "how did they combine these three pieces?".

8. So what order do we read in?

The map also settles the reading order: start from the bottom of the arrows, the side that others lean on. spring-beans (how a bean is created) → spring-context (refresh() and annotation configuration) → spring-aop (proxies) → then data and web on top. spring-core is not read as a separate block; the needed parts are pulled out whenever the reading gets stuck on them. How the reading is divided into units and how far it has got is laid out in Part 1.

The reading order. ① spring-beans (how a bean gets made, Parts 3–6) → ② spring-context (refresh() and annotation configuration, Parts 7–9) → ③ spring-aop (proxies, Parts 10–11) → ④ data (Parts 17–19) and web (Parts 20–25) on top. spring-core is not read on its own; it is opened whenever the first three steps get stuck on it The bottom-up reading order, with part numbers. Dotted lines mean "opened when needed".

Recap

What this post left out

To keep the map to one page, these were left out.

What to read next

Time to fold the map and walk into the first destination. The next post is about the beans.factory packages of spring-beans: why the BeanFactory interface is split into several, which classes are stacked up on the way to DefaultListableBeanFactory, which implements them all, and what is actually written in a BeanDefinition — the thing section 6 only called a "blueprint".

Digging into Spring's Source 3: The BeanFactory Hierarchy and BeanDefinition

More posts