Posts

Building a Complete Spring Boot API

Eight posts ago, this track started with a single endpoint returning a hardcoded string. Since then we've added real controllers, validation at the trust boundary, JPA persistence, transactions that don't half-succeed, Problem Details errors, and JWT security. Every one of those posts ended with "here's the piece" — this one is "here's the machine." We're going to assemble every piece into one coherent Orders API: controller → service → repository, validated, transacted, secured, and proven by an integration test. Nothing here is new. That's the point. If each earlier post did its job, this one should feel like snapping Lego bricks together — and any step that feels unfamiliar tells you exactly which post to revisit. The build plan One service, three layers, three cross-cutting concerns. The layers only talk downward; the concerns wrap the layers: HTTP client Controller — @RestController H...

Spring Security Basics: Auth for REST APIs

Two weeks after our checkout API went live, an engineer on the ops team pasted something into Slack that froze the channel: a curl command, run from his laptop, that listed every pending refund in the system. No login, no token, no error. The endpoint /api/admin/refunds had shipped with no authentication at all — we'd built the refund feature, tested it, and never asked who was allowed to call it. This post is the fix: the minimum Spring Security you need to protect a REST API, done the modern way. This is deliberately basic . A later track goes deep on security — OAuth2 login flows, multi-tenancy, key rotation, threat modeling. Here: the filter chain, passwords, HTTP Basic for internal tools, JWT for real APIs, and method-level authorization. Enough to ship an API that isn't open to the internet. The 30-second mental model: a chain of filters Spring Security is not a wall around your controller. It's a chain of servlet filters that every request passes through bef...

Transactions: @Transactional, Isolation & Propagation

A month after our checkout service launched, support forwarded a ticket that read like a riddle: "I was charged, but my order never shipped." The payment gateway showed a captured charge for $149.99. Our database showed an order row stuck in PENDING — with inventory reserved for an order we would have to cancel and refund by hand. The money was real; the database disagreed with the gateway about what had happened. The cause was a method that did three writes — save the order, reserve inventory, charge the card — with no transaction around them. The charge succeeded, a downstream call threw, and each write had already committed on its own. The fix wasn't better error handling. It was declaring the whole method one atomic unit of work. That declaration is @Transactional — and this post is about what that annotation actually does, the proxy that makes it work, and the three ways it surprises people in production. ACID in one paragraph A transaction is a contract with...

JPA Performance: LAZY/EAGER, N+1, Fetch Joins & Locking

The "order history" page loaded in 80 milliseconds in every demo. In production, for the customer's account with 500 orders, it took 11 seconds. Nobody had changed the code between the demo and the launch — the difference was the data. The page loaded the orders with one query, then, for each order, lazily loaded its line items with another query. One plus five hundred: 501 round trips to the database, each fast, together catastrophic. The APM trace was almost comical — the same SELECT repeated 500 times with a different id. This is the N+1 problem , and it is the single most-asked JPA topic in backend interviews — because it's the place where the ORM's convenience quietly becomes a performance bug. This post demonstrates it with real, countable queries, fixes it two ways ( JOIN FETCH and @EntityGraph ), settles LAZY vs EAGER, and then handles the other classic: two threads updating the same row, solved with optimistic locking. The N+1, demonstrated and coun...

Spring Data JPA: Entities, Repositories & Relationships

The "customer order history" screen was the checkout service's most ordinary feature: given a customer, show their orders. The first version was a 40-line DAO with hand-built SQL — string concatenation, a hand-rolled row mapper, and a WHERE clause assembled from three optional filters. It worked for months. Then a refactor dropped one filter condition on a code path nobody tested, and for eleven minutes the screen showed every customer's orders to every customer . No data was modified, but the incident report's root cause was one sentence long: the relationship between customers and orders existed only inside a SQL string. JPA — the Jakarta Persistence API — makes that relationship explicit, in Java, where the compiler and your tests can see it. You describe your domain as classes; Hibernate (the JPA provider under Spring Boot 4.1) translates between those classes and your tables. And Spring Data JPA goes one step further: for most queries, you don't even...