What Spring Boot is
Spring (2003) is Java's dominant application framework; Spring
Boot (2014) is Spring with the configuration agony removed — sensible
defaults, embedded server, one main() and you have a production-grade
service. It is the employment heavyweight: most banks, most enterprises,
most of Amazon-scale Java runs on it.
Where Node gives you a minimal toolkit, Spring gives you an architecture. Its core idea is worth learning even if you never write Java again.
The big idea: Inversion of Control / Dependency Injection
You learned in Functions and OOP & SOLID that hidden dependencies make code untestable. Scale that to an app with 400 classes: who constructs what, in what order, with which config?
Spring's answer: you don't construct anything. You declare classes as components and what they need; the framework builds the object graph — the IoC container — and hands each class its dependencies (dependency injection):
@Service // "Spring, manage this class"
public class OrderService {
private final OrderRepository repo; // what I need
private final PaymentClient payments;
public OrderService(OrderRepository repo, PaymentClient payments) {
this.repo = repo; // Spring calls this constructor
this.payments = payments; // with real instances — DI
}
}
Why it matters beyond convenience: every class depends on interfaces it asks for, never on construction details — which is Dependency Inversion enforced by a framework. Tests inject mocks through the same constructor. Swapping Postgres for a stub, or a real payment client for a fake, touches zero business code. (StockStump is this architecture live — Spring WebFlux flavor.)
The standard layering
Spring codebases follow one shape so universally that interviews assume it — controller → service → repository:
@RestController
@RequestMapping("/api/orders")
public class OrderController { // HTTP ↔ objects, nothing else
private final OrderService service;
public OrderController(OrderService service) { this.service = service; }
@GetMapping("/{id}")
public OrderDto get(@PathVariable long id) {
return service.find(id); // 404 via exception handler
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public OrderDto create(@Valid @RequestBody CreateOrderRequest req) {
return service.create(req); // @Valid: schema-check input
}
}
@Service
public class OrderService { // the rules live here
@Transactional // ← see below
public OrderDto create(CreateOrderRequest req) {
Order order = new Order(req);
inventory.reserve(order.items()); // both succeed
return OrderDto.from(repo.save(order)); // or neither does
}
}
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByUserId(long userId); // Spring WRITES this query
}
That repository interface with no implementation is Spring Data JPA:
it generates the SQL from the method name. JPA/Hibernate is the
ORM (Object-Relational Mapper) — Order objects map to an orders
table, and you mostly stop writing SQL. The
Level 9 warning applies: ORMs hide queries,
and hidden queries become the N+1 problem — know what SQL your method
names generate.
@Transactional — the annotation that earns its interview question
One annotation wraps the method in a database transaction: everything inside commits together or rolls back together on exception. The reserve-and-save above can never half-happen.
The mechanism is worth understanding, not just naming. A bean is
Spring's word for any object the container manages (your OrderService
instance is a bean). When a bean has @Transactional, Spring doesn't
hand out your object directly — it hands out a proxy: a stand-in
object that wraps yours, like a receptionist in front of an office. Calls
arrive at the proxy, which opens the transaction, then forwards to your
real method, then commits or rolls back when it returns. This explains
the famous gotcha: a method calling another method on the same class
(this.create(...)) goes straight to the real object — the receptionist
never sees it — so the annotation silently does nothing. A favorite
mid-level interview question, because it tests whether you know the
mechanism or just the annotation.
Annotations, magic, and the trade
Spring is driven by annotations: @Service, @GetMapping,
@Transactional, @Valid. The trade-off is honest: enormous leverage,
at the cost of magic — behavior that happens around your code where
you can't see it. The framework's learning curve is learning what each
annotation actually does (proxies, filters, the request pipeline — the
same middleware idea, with more machinery).
Threading model: classic Spring MVC is thread-per-request — the model Node defined itself against. Modern JVMs changed the economics: virtual threads (Java 21+) make threads cheap enough to have both the simple blocking style and high concurrency. The reactive alternative — WebFlux, Spring's event-loop flavor — is what StockStump uses; same concepts as Node's loop, Java syntax.
Production perspective
- Why enterprises choose it: 20 years of hardening, security/metrics/
transactions as integrated modules (Actuator's
/healthendpoint is the industry's de-facto load-balancer probe), an inexhaustible hiring pool, and a structure 100 engineers can share without negotiation. - Spring Boot specifically = embedded Tomcat (the JAR is the
server, perfect for containers, Level 10),
auto-configuration, starters per concern (
web,data-jpa,security). - The cost, honestly: startup seconds and memory hunger vs Node/Go — which is why serverless/edge tiers rarely use it, and why GraalVM native images exist.
Common mistakes
- Business logic in controllers — untestable without HTTP, unusable from jobs/queues. Controllers translate; services decide.
- Field injection (
@Autowiredon fields) — hides dependencies, breaks plain-Java testing. Constructor injection (above) is the standard; it makes god objects visible (a constructor with nine arguments is a design smell you can see). @Transactionalon private/internal calls — silently no-ops through the proxy gotcha.- Returning JPA entities from controllers — leaks schema, triggers lazy-loading surprises mid-serialization. Map to DTOs at the boundary.
- Ignoring the SQL —
findAll()then filtering in Java; N+1 lazy loads in a loop. The ORM is a convenience, not an excuse (Level 9 makes you read query plans).
Interview perspective
Practice
- Build: the three-layer Order API above via start.spring.io (web + data-jpa + H2). Verify 201/404/400 paths with curl.
- Prove DI to yourself: write a
FakePaymentClientand unit-testOrderServicewith no Spring and no database — constructor injection makes it plain Java. - Trigger the gotcha: make a method call
this.transactionalMethod()internally, throw inside, and observe the missing rollback. Fix it by extracting the bean. - Connect: read StockStump's deep-dive and map its WebFlux choice to the threading discussion above — why does a trading app with WebSocket fan-out pick the reactive flavor?
Next: Python FastAPI — the third philosophy: type hints as the framework.