One idea sits under the whole module
If you step back from the nine topics in M1 Core, they are all answers to a single question: how does Spring change what your beans do without you editing their code?
You write a plain service class. Somewhere between "Spring reads it" and "someone calls it," extra behaviour appears — a transaction opens, a log line fires, a security check runs — and none of it is written in your method. That gap between the class you wrote and the object that actually runs is where this entire module lives.
Spring has exactly two moments where it reaches into that gap. One is at startup, while beans are being built, using post-processors. The other is at every call, after the bean exists, using a proxy. Almost every topic in M1 Core is either one of those two engines or something built on top of them. Let's walk the chain in that order.
The startup engine: post-processors
Before any of your code runs, Spring assembles the container. It has two official hooks that let framework code — and yours — step into that assembly. Both were the first topic of the module, and everything else leans on them.
A BeanFactoryPostProcessor runs before beans exist. At that stage Spring holds only the blueprints — the bean definitions, one per bean, describing what to build. A BeanFactoryPostProcessor can edit those blueprints: change a scope, swap a class, resolve a ${...} placeholder. It never sees a real object, only the recipe.
A BeanPostProcessor runs after each bean is constructed, on the instance itself. Spring hands it every freshly built bean and lets it return either that same object or a replacement.
That "return a replacement" clause is the hinge of the whole module. It is exactly how a proxy gets in.
public interface BeanPostProcessor {
Object postProcessAfterInitialization(Object bean, String name);
// return `bean`, or return something that wraps it
}
The one thing to remember: post-processors are Spring's public seam for altering beans during startup — definitions first (BeanFactoryPostProcessor), then instances (BeanPostProcessor). The proxying you meet next is not a separate feature; it is a BeanPostProcessor handing back a wrapper instead of your bean.
The runtime engine: proxies
A proxy is a stand-in object of the same type as your bean, placed in front of the real one so it can run extra code before and after each method. Callers think they hold your service; they actually hold the wrapper.
Spring builds that wrapper two ways, and knowing which one matters. A JDK dynamic proxy works only when your bean implements an interface — it creates a brand-new class implementing that same interface and forwards calls to your object. CGLIB needs no interface: it creates a subclass of your class at runtime and overrides each method to add the extra behaviour. Spring Boot defaults to CGLIB so it works whether or not you wrote an interface.
The subclass mechanism explains a set of limits you saw repeated all module:
- A call the bean makes to itself (
this.method()) never leaves the object, so it never crosses the proxy — the extra behaviour is skipped. - A
privateorfinalmethod can't be overridden by the subclass, so it can't be wrapped either.
The one thing to remember: the proxy is how Spring adds behaviour at runtime, and its "only wraps calls entering from outside, only overridable methods" nature is the root cause of gotchas you'll keep meeting — most famously in transactions.
AOP: the vocabulary for "code around methods"
Post-processors and proxies are the machinery. AOP — aspect-oriented programming — is the vocabulary Spring uses to talk about it, and the general-purpose way to attach behaviour without a proxy per feature.
The problem it names is the cross-cutting concern: logging, security, transactions — ceremony that repeats across many unrelated methods and would clutter every one of them. AOP pulls that ceremony out into one place. Four words carry the model:
- Advice — the extra code to run (e.g. "start a transaction, then commit").
- Pointcut — an expression selecting which methods the advice applies to.
- Aspect — advice plus pointcut bundled together as one unit.
-
Weaving — the act of wiring the advice into the target. In Spring, weaving is the proxy: at startup a
BeanPostProcessorsees a bean matched by some pointcut and returns a proxy whose job is to run the advice around your method.
So AOP is not a new engine. It is the naming and configuration layer over the same proxy you just met.
The one thing to remember: aspect = advice + pointcut; weaving in Spring means "wrap the bean in a proxy." Every "magic annotation" that adds behaviour is an aspect underneath.
@Transactional: AOP's flagship application
Now the pieces converge. @Transactional is just an aspect. Its advice is the TransactionInterceptor living inside the proxy, and it runs the begin / commit / rollback ceremony you would otherwise hand-write:
TransactionStatus tx = txManager.getTransaction(...); // begin
try {
Object result = method.invoke(target, args); // your method body
txManager.commit(tx); // commit on success
return result;
} catch (RuntimeException | Error ex) {
txManager.rollback(tx); // rollback on failure
throw ex;
}
The interceptor never touches a Connection; it delegates to a PlatformTransactionManager, which opens a connection and parks it on the current thread so every repository on that thread silently joins the same transaction.
Two facts explain nearly every transaction surprise, and both trace straight back to the two engines:
-
Rollback only fires on an unchecked exception that escapes the method. A checked exception commits; a swallowed exception commits. This is the interceptor's
catch (RuntimeException | Error)rule, not a bug. -
Self-invocation silently starts no transaction. A
this.save()call never crosses the proxy, so the interceptor never runs — the exact proxy limit from two sections ago.
The one thing to remember: @Transactional is a proxy + an interceptor. When it "doesn't work," you are almost always looking at the checked-exception rule or the self-invocation gap.
Propagation: what happens when transactions meet
@Transactional gets interesting when a transactional method calls another transactional method. Propagation is the setting that decides what the inner call does about the outer transaction already in flight.
- REQUIRED (the default) — join the existing transaction if there is one, otherwise start a new one. Inner and outer share one commit; a rollback anywhere dooms the whole thing.
- REQUIRES_NEW — suspend the outer transaction and run in a genuinely independent one. It commits or rolls back on its own, whatever the outer does. Useful for an audit log you want kept even if the main work fails.
- NESTED — one transaction with a savepoint; the inner part can roll back to the savepoint without killing the outer work.
The one thing to remember: REQUIRED merges everything into one all-or-nothing unit; REQUIRES_NEW carves out an independent one. Reach for REQUIRES_NEW only when you truly want the inner work to survive an outer failure.
Isolation: the part Spring only forwards
Propagation is Spring's own concept. Isolation is not — it is a database guarantee about how concurrent transactions see each other's uncommitted work, and Spring merely passes your chosen level down to the connection.
The levels are best understood by the anomalies they prevent, from weakest to strongest:
- READ_UNCOMMITTED — you can see another transaction's uncommitted writes (a dirty read).
- READ_COMMITTED — you see only committed data, but the same row can change if you read it twice (a non-repeatable read).
- REPEATABLE_READ — a row you've read stays stable, but new rows matching your query can still appear (a phantom read).
- SERIALIZABLE — transactions behave as if run one at a time; no anomalies, most contention.
The one thing to remember: propagation is Spring's; isolation belongs to the database, and Spring just hands it the level. Stronger isolation buys correctness with concurrency.
SpEL: the little language woven through
Threaded across these features is SpEL — the Spring Expression Language — a small language for expressions that are written as strings and evaluated at runtime, not compile time. It is why @Value("${server.port}"), security rules like @PreAuthorize("hasRole('ADMIN')"), and conditional bean wiring can all carry logic inside an annotation.
@Value("#{2 * T(java.lang.Math).PI}") // arithmetic, static calls
private double circleConstant;
The #{...} marks a SpEL expression; ${...} is plain property lookup. SpEL can read bean properties, call methods, and reference other beans — enormous flexibility, evaluated late, which is also why a bad expression fails only at runtime.
The one thing to remember: SpEL is runtime logic-in-a-string. It makes annotations powerful, at the cost of errors that surface only when the expression runs.
Application events: decoupling without a proxy
The last topic steps outside the proxy world. Application events are Spring's in-process publish/subscribe: one bean announces that something happened, and other beans react — without either holding a reference to the other.
publisher.publishEvent(new OrderPlaced(order)); // announce
@EventListener
public void onOrder(OrderPlaced e) { /* react */ } // subscribe
The publisher doesn't know who listens; listeners don't know who published. That is the decoupling. The sharp edge: by default, publishing is synchronous — publishEvent runs every listener on the caller's thread before returning, and inside a transaction the listeners are part of that same transaction. Add @Async (or listen at a transaction phase) when you want them to run separately.
The one thing to remember: events decouple who talks to whom, but by default they are synchronous and share the caller's thread and transaction — they are not a background queue unless you make them one.
The single model to carry out of M1 Core
Everything above collapses into one sentence: Spring adds behaviour around your beans without touching their code, using post-processors at startup and proxies at every call.
BeanFactoryPostProcessor edits the blueprints and BeanPostProcessor edits the instances — and it is a BeanPostProcessor that swaps in a proxy. The proxy (JDK or CGLIB) is the runtime engine, and AOP is the vocabulary for configuring it: aspect, pointcut, advice, weaving. @Transactional is that machinery's flagship aspect, which is why its two famous gotchas — the unchecked-exception rule and self-invocation — are really proxy facts. Propagation and isolation tune what that transaction does when transactions meet or run concurrently. SpEL is the runtime expression language woven through the annotations, and application events are the one decoupling mechanism that steps outside the proxy — synchronous by default.
Hold the two engines — post-processor and proxy — and every feature in this module stops being separate magic and becomes one idea seen from different angles.
Top comments (0)