Spring vs Plain Java
Spring vs Plain Java
After eight lessons building up Spring's mental model, a legitimate question emerges: what does the container actually buy us? Plain Java can instantiate objects, call setters, and pass dependencies through constructors — everything Spring does mechanically. This lesson answers that question concretely, by walking through the same application wired by hand and then wired by Spring, then naming every advantage and every cost that comes with the container.
The Hand-Wired Baseline
Imagine a small order-processing service. Three collaborators: a ProductRepository that reads from a database, an InventoryService that checks stock, and an OrderService that orchestrates everything.
This compiles, runs, and is fully testable. But notice what you are responsible for:
- Building the dependency graph in the correct order.
- Creating every collaborator exactly once (or tracking which ones are singletons yourself).
- Closing resources (
HikariDataSourceisAutoCloseable) at shutdown. - Re-wiring by hand every time a dependency changes or a new scope is needed.
The Spring-Wired Version
The application logic is identical. What changed is who is responsible for the wiring — and that shift carries a concrete list of benefits.
Benefit 1: Automatic Dependency Resolution
Spring reads the constructor parameter types, finds beans that satisfy them, and wires everything. Add a new dependency to OrderService and you update one constructor — not a cascade of manual calls in Main. In a system with dozens of services this matters enormously.
@Autowired on single-constructor beans (Spring injects automatically since 4.3). Field injection hides dependencies and breaks testability — avoid it.
Benefit 2: Singleton Scope Without Global State
By default every Spring bean is a singleton: one instance per container, shared across all callers. In plain Java you either use a static field (true global state, hard to test) or coordinate object creation manually. Spring gives you singletons without the anti-pattern: the container owns the instance; you inject it.
Benefit 3: Lifecycle Management
Resources that need to be closed (database pools, thread pools, open files) are error-prone in hand-wired code. Spring's lifecycle hooks handle it automatically:
Without the container you must hook into JVM shutdown yourself (Runtime.getRuntime().addShutdownHook(...)) and remember to do it for every resource-owning class.
Benefit 4: Scoped Beans
Web applications often need objects that live for exactly one HTTP request. Hand-wiring that requires ThreadLocal variables, careful cleanup, and a lot of discipline. Spring's request scope encapsulates all of it:
The container creates the instance when the request arrives and destroys it when the response is committed. Zero ThreadLocal code in your service layer.
Benefit 5: AOP — Cross-Cutting Concerns Without Boilerplate
Logging, transaction management, and security checks appear in dozens of methods. In plain Java you copy-paste that boilerplate or write a wrapper for every class. Spring's AOP proxy model applies aspects transparently:
Without Spring you write the try/commit/catch/rollback block everywhere transactions are needed — and forget it once, and you have a data-consistency bug in production.
The Honest Trade-offs
Spring is not free. Know the costs before you commit:
- Startup time: The container scans classes, proxies beans, and resolves the graph at launch. A large Spring Boot application can take 10–30 seconds to start, which matters for CLI tools and serverless functions. Plain Java starts in milliseconds.
- Implicit magic: When wiring is invisible, debugging a missing bean or a circular dependency requires understanding the container's internals. Plain Java wiring is transparent — errors point directly at the constructor call that fails.
- Classpath weight: The Spring ecosystem adds several megabytes of JARs. Trivial for a backend service, noticeable for embedded or constrained environments.
- Learning curve: Developers must understand scopes, proxy modes, and the application context before they can diagnose problems confidently.
When Plain Java Is Enough
- Command-line utilities with a single entry point and two or three dependencies.
- Library JAR code — never force a container on your library's users.
- Performance-sensitive startup paths where cold-start latency matters (AWS Lambda functions, CLI tools called frequently).
- Code where complete transparency matters more than convenience (security-critical modules, low-level drivers).
Summary
The Spring IoC container earns its place by automating four things you would otherwise own manually: dependency graph resolution, singleton lifecycle, resource cleanup, and cross-cutting concerns via AOP. Plain Java remains the right tool for small, self-contained programs where startup speed, transparency, or zero-dependency deployment matters more than those benefits. For professional server-side applications — anything with a database, a transaction boundary, or more than a handful of collaborating services — the container's benefits far outweigh its costs. With that trade-off fully understood, the final lesson puts it all together in a complete wiring project.