// Transmissions — What We Shipped
Spring is not a thing to memorise. It is six or seven mechanisms, each solving a problem you have already met in Laravel, assembled into a default answer. Learn the mechanisms and every Java framework — Spring, Quarkus, Micronaut, Jakarta EE — reads as a variation.
You know this one already: Laravel's service container. A framework builds an object graph for you, decides what each object needs, and hands it over. In Java that idea is load-bearing rather than convenient, and it has a name — inversion of control.
The pieces are always the same. A bean is an object the container owns. A definition says how to make one. Injection supplies its collaborators — overwhelmingly through the constructor, which is worth insisting on: a constructor-injected object cannot exist in a half-built state, and its dependencies are visible in one signature. A scope says how many exist: singleton by default (one for the whole process — remember Part I, that means shared across every concurrent request), or per-request, or per-prototype.
The container turns "what depends on what" from something buried inside constructors into a graph the framework can see. Once it can see the graph it can do things you cannot easily do by hand: swap an implementation in tests, wrap every method of a class in a transaction, start things in dependency order, and tell you at boot — not at 3am — that something is missing.
Auto-configuration is the Spring Boot idea layered on top: a starter dependency carries conditional bean definitions that say "if a database driver is on the classpath and the user has not defined their own DataSource, define one". That is why adding a dependency appears to configure itself. It is also why Java people say "convention over configuration" and mean something slightly different from Rails — here the conventions are conditional, and the escape hatch is defining the bean yourself.
Magic you cannot explain is a liability. Every serious Spring app eventually needs someone who can read the condition-evaluation report and say why a bean did or did not appear. Learn where that report is before you need it.
Here is the mechanism that explains most Java "magic". When you mark something transactional, cacheable, retryable or secured, the container does not rewrite your method. It hands out a proxy: an object of the same interface or subclass that wraps yours, runs behaviour before and after, and delegates in the middle. Everyone else gets the proxy; they cannot tell.
That is aspect-oriented programming in practice, and the vocabulary is worth having: an aspect is the cross-cutting concern, a pointcut selects where it applies, and advice is what runs. Laravel's middleware is the same shape restricted to HTTP; this is the general form, applied to any bean.
Self-invocation. If a method inside your class calls another method of the same class directly, the call never leaves the object, so it never passes through the proxy, so the annotation does nothing. Your transaction silently does not exist. It is not a bug in the framework; it is a consequence of proxying, and it is invisible in a code review unless you know to look. Every Java developer learns this once, usually expensively.
Underneath sits the servlet model: a container (Tomcat, Jetty, Undertow) owns sockets and threads and hands you a request and a response. Above it:
| Component | Job | Laravel equivalent |
|---|---|---|
| Filter | Wraps every request at container level — before routing exists | Global middleware |
| Dispatcher | The front controller everything passes through | index.php + kernel |
| Handler mapping | Chooses which method serves this URL | Router |
| Argument resolver | Turns the raw request into typed method parameters | Route-model binding, form requests |
| Message converter | Body ⇄ object, by content type | JSON resources / casting |
| Interceptor | Wraps handlers once routing is known | Route middleware |
| Exception handler | Turns thrown failures into responses | Exception handler |
Two ideas earn their keep here. Content negotiation: the same handler can answer JSON or something else based on headers, because conversion is a separate stage. And validation as a declared contract: constraints are annotations on the model, checked at the boundary, so invalid input never reaches your logic.
This is the part that will cost you the most, so it deserves the most care. JPA is the specification, Hibernate the usual implementation, and Spring Data a convenience layer above both. Eloquent it is not: Eloquent is Active Record — a model is a row and knows how to save itself. JPA is a Data Mapper with a unit of work, and the difference is the whole game.
The engine is the persistence context. Within a transaction it is:
Lazy loading is the other half: a relation is a proxy until touched, at which point it fetches. Convenient, and the source of the two classic failures.
The N+1 query. Load a hundred orders, touch each one's customer, and you have issued a hundred and one queries. Nothing errors; the endpoint is just mysteriously slow. The fix is to declare what you need up front (a fetch join, an entity graph). Learn to spot it by counting queries, not by reading code.
Lazy loading outside the transaction. The context closes at the transaction boundary; touching an untouched relation afterwards — typically while serialising the response — throws. The real fix is not "keep the session open longer" but deciding, deliberately, what the boundary loads and returning a purpose-built response object rather than the entity itself.
A transaction has a boundary (where it starts and commits), a propagation rule (what happens when a transactional method calls another — join the existing one, suspend it, demand a new one), and an isolation level (what concurrent transactions can see of each other). Framework annotations make boundaries invisible, which is precisely why you must be able to state where yours are. And under concurrency you want optimistic locking — a version column, so a stale write is rejected instead of silently overwriting someone else's.
In Eloquent you tell the database what to do. In JPA you manipulate an in-memory graph of objects and the framework works out what the database must be told, at the end, in an order it chooses. Everything good and everything painful about it follows from that inversion.
Configuration is external, layered and bound to typed objects: files, environment variables and command-line all merge by precedence, then get bound to a class whose fields are validated at startup. Profiles activate whole sets of beans and settings by environment. The discipline worth adopting: config errors should stop the process at boot, loudly, rather than surface as a null three hours later.
Because the process is long-lived, you can ask it how it is doing — a thing PHP cannot meaningfully offer. Expect four things as standard: health endpoints (liveness versus readiness, and they are different questions), metrics via a façade that exports to whatever you run, structured logs with a correlation id threaded through a request, and traces that follow one operation across services. Treat these as part of the application, not ops decoration.
The container is what makes Java testing distinctive: because the graph is declared, you can boot a slice of it — the web layer with the service mocked, or the persistence layer against a real database in a throwaway container — and test at exactly the altitude you mean. The three altitudes are worth naming: unit (no framework at all, pure objects), slice (part of the container), integration (the real thing, real database). Most bad Java test suites are bad because everything was written at the wrong altitude.
Part III — the ten technologies →
Written for someone who has an agent for the syntax and needs the ideas instead. Nothing here is a code sample on purpose — if you can name the mechanism, you can ask for the code and judge what comes back.