Study Notes
Spring Boot by Buildingcapstone

Build from scratch

Independent capstones with specs, acceptance checks, task breakdowns, and hidden guidance so the learner can transfer Spring Boot skills to new projects.

90 min read·verified Spring Boot 4.x·reviewed 2026-08-28

After this you should be able to

  • Plan an independent Spring Boot build
  • Demonstrate transferable Spring skills
CapstonesSpecsDIY

Spring Boot by Building · Chapter 14

No TaskFlow to lean on. Each project below is a real spec with a task breakdown — the way work actually arrives. You start from an empty start.spring.io and build the whole line yourself. The first project holds your hand through the breakdown; the later ones let go.

Chapter 14 · You build — scaffold off · 6 capstones + a repeatable ritual


Choose your route#

Study the same Build from scratch explanation with a different purpose: build and verify it in Learn deeply, or retrieve and defend it in Prepare for interviews.

Learning goal
  • Plan an independent Spring Boot build
  • Demonstrate transferable Spring skills

Shared core#

The chapter below remains the canonical explanation for both goals. Keep its model, examples, and trade-offs shared; only the deliberate practice changes with the route.

Learn deeply: build evidence#

Use this chapter’s TaskFlow work to make one small, observable change. Run the stated acceptance checks before recording practice.

Prepare for interviews: explain the trade-off#

Without notes, explain the boundary this chapter protects, one concrete implementation decision, and the cost of a tempting shortcut. Then answer one follow-up using the chapter’s own example.

Learn next: Use the completed evidence to choose a focused review or a new independent task. Interview next: Use plan an independent spring boot build as the base, then structure a concise spring explanation in Spring mock round 1 — core backend fundamentals.

Continue to Return to Spring roadmap

// the goal of the whole course, cashed in here

If you can take a plain-English spec and produce a working, layered, tested Spring Boot API without copying an existing project, the confusion you started with is gone. That's this chapter. Each capstone gives you a feature/task breakdown — the exact artifact you asked for — so you're never staring at a blank page, only at the next task.

Section 1The starter ritual — memorise this sequence#

Every new Spring project begins the same way. Internalise this and "getting started" stops being a decision each time — it's a reflex.

  1. Blueprint first

    Before generating anything, fill the Chapter 7 template: nouns→data, verbs→operations, assign to layers, decide abstractions, sketch boxes+arrows and a build order. Ten minutes here saves an hour of thrash — you'll know every class before you make the project.

  2. Generate

    start.spring.io → Maven, Java, current stable Boot, Jar. Dependencies: Spring Web, Spring Data JPA, H2, Validation, Lombok. (That's the workhorse set for every project here.)

  3. Configure

    application.properties: the H2 block from Chapter 4 + show-sql=true + h2.console.enabled=true. Do this before writing code.

  4. Make the packages

    controller, service, repository, model, dto under your base package. Empty folders, ready to fill.

  5. Build bottom-up, one entity at a time

    Model → Repository → Service → Controller. Run after each layer. Get one endpoint green before adding the next feature.

  6. Verify & test

    curl / H2 console for a manual pass; one Mockito service test per rule. Then move to the next task.

// the discipline that prevents "everything got messed up"

Build and run one vertical slice at a time — a single feature all the way down the layers — rather than writing all controllers, then all services. A slice that works end-to-end is a checkpoint you can't fall behind. This is the single habit that turns a scary big app into a stack of small finished pieces.


Section 2Two techniques the capstones need#

The capstones below ask for two things the walkthroughs only touched. Here they are, worked once, so you can drop them in when a spec calls for them.

Global error handling with @ControllerAdvice#

So far each exception carried its own @ResponseStatus (Chapter 3). That works, but real specs ask for consistent error responses and central handling. A single @RestControllerAdvice class catches exceptions from every controller and turns them into a uniform JSON body — one home for all error mapping.

web/ApiError.java + web/GlobalExceptionHandler.java
public record ApiError(int status, String error, String message, Instant timestamp) { }

@RestControllerAdvice                        // one advisor for ALL controllers
public class GlobalExceptionHandler {

    // your "not found" family -> 404, uniform body
    @ExceptionHandler(TaskNotFoundException.class)
    public ResponseEntity<ApiError> notFound(RuntimeException ex) {
        return build(HttpStatus.NOT_FOUND, ex.getMessage());
    }

    // bean-validation failures (@Valid) -> 400 with the first field message
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiError> invalid(MethodArgumentNotValidException ex) {
        String msg = ex.getBindingResult().getFieldErrors().stream().findFirst().map(f -> f.getField() + " " + f.getDefaultMessage()).orElse("Validation failed");
        return build(HttpStatus.BAD_REQUEST, msg);
    }

    // a business-rule breach -> 409
    @ExceptionHandler(IllegalStateException.class)
    public ResponseEntity<ApiError> conflict(IllegalStateException ex) {
        return build(HttpStatus.CONFLICT, ex.getMessage());
    }

    private ResponseEntity<ApiError> build(HttpStatus s, String msg) {
        return ResponseEntity.status(s).body(new ApiError(s.value(), s.getReasonPhrase(), msg, Instant.now()));
    }
}
why this is the grown-up approach
  • One class maps each exception type to a status + clean body. Controllers and services just throw; they never format errors. That's the same "one home per concern" rule, applied to error handling.
  • Once you have this, you can drop the per-exception @ResponseStatus annotations (or keep them — an explicit @ExceptionHandler here wins). The specs' "return appropriate HTTP codes" requirement is satisfied in one place.

Many-to-many — and why you usually want a join entity#

Grocery's "an order has many items, an item is in many orders" is a many-to-many. The naive mapping is @ManyToMany, but the moment you need data about the pairing (how many of this item in this order?), you promote the join to its own entity. That decision is the whole lesson.

Plain @ManyToMany#

  • Just links two tables via a hidden join table
  • No room for extra fields (quantity, line price)
  • Fine for tags↔posts; too thin for orders↔items
  • An OrderItem row = one line of an order
  • Holds quantity, unit price — real data
  • Two @ManyToOnes replace one @ManyToMany
the join-entity shape (Grocery)
@Entity
class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id;
    @ManyToOne Customer customer;                 // an order belongs to one customer
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    List<OrderItem> items = new ArrayList<>();     // and has many line items
    LocalDate orderDate;
    BigDecimal totalPrice;
}

@Entity
class OrderItem {                                 // the promoted join
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id;
    @ManyToOne Order order;                        // which order
    @ManyToOne GroceryItem groceryItem;           // which item
    int quantity;                                 // the extra data a plain M2M can't hold
}
the rule to remember
  • Pure link (no extra fields) → @ManyToMany. Link that carries data → a join entity with two @ManyToOnes. Orders always carry data (quantity, price), so they get a join entity.
  • totalPrice is a derived value — compute it in the service from the line items, don't trust the client to send it. (A rule → the service, per Chapter 7.)

Capstone 1 · guided breakdownBookmark Keeper#

A tiny API to save links you want to read later. Single entity — a clean rehearsal of everything from Chapters 2–5, now with no scaffold code, only a task list.

Spec you build

What Bookmark Keeper must do#

A Bookmark has: id, url (required), title, tag (e.g. "reading", "work"), favorite (boolean, default false), and createdAt (set by the server). The API supports full CRUD, filtering by tag, and listing favorites.

  • Create a bookmark; blank url → 400.
  • List all; get one by id; missing id → 404.
  • Update and delete by id.
  • Filter: list bookmarks by tag; list only favorites.
  • createdAt is set automatically on create, never by the client.
  • At least two Mockito unit tests on the service (one happy path, one 404).

Feature / task breakdown — do them in order; run after each:

  1. Blueprint it with the Chapter 7 template, then run the starter ritual (Section 1). Project artifact: bookmarks.
  2. Model: Bookmark entity with the six fields. @Id @GeneratedValue on id; @Column(nullable=false) on url; Lombok @Getter/@Setter/@NoArgsConstructor.
  3. Repository: interface BookmarkRepository extends JpaRepository<Bookmark, Long>. Add derived queries findByTag(String) and findByFavoriteTrue().
  4. Service: methods getAll, getById (throws 404), create, update, delete, byTag, favorites. Set createdAt = LocalDateTime.now() inside create.
  5. 404 type: a BookmarkNotFoundException with @ResponseStatus(NOT_FOUND) (copy the pattern from Chapter 3).
  6. DTO: BookmarkRequest record with @NotBlank url, plus title, tag, favorite. No id, no createdAt — the server owns those.
  7. Controller: @RequestMapping("/bookmarks") + the CRUD routes, plus GET /bookmarks?tag=work and GET /bookmarks/favorites. Use @Valid.
  8. Verify by hand: curl each route; open the H2 console and confirm the table + a row.
  9. Test: BookmarkServiceTest — create sets createdAt & favorite defaults; getById missing throws. Mock the repository.
Definition of done: every acceptance check passes via curl, the H2 table matches your entity, and ./mvnw test is green. If yes — you built a Spring Boot service from a spec, alone.
Stuck on one task? Open targeted nudges (not a full solution)

createdAt not appearing? You set it in the service's create, not the DTO. The client can't send it.

Filtering by tag AND listing all from one route? Make tag a @RequestParam(required=false); if null, return all, else byTag — exactly the pattern from Exercise 4.1.

404 returning 500 instead? Your exception is missing @ResponseStatus(HttpStatus.NOT_FOUND), or the service isn't throwing it.

This capstone is deliberately the same shape as TaskFlow. If a task stumps you, find the matching piece in Chapters 3–5 and adapt it — that transfer is the skill being certified.


Capstone 2 · lighter scaffold + a relationshipReading Shelf#

One new concept: a relationship between two entities. An Author has many Books. This is the thing most tutorials fumble; you'll meet it with the layer map intact.

Spec you build

What Reading Shelf must do#

An Author has id, name. A Book has id, title, year, a rating (1–5), and belongs to one author. You can add authors, add books to an author, list an author's books, and list top-rated books across all authors.

  • Create an author; create a book under an author (e.g. POST /authors/1/books).
  • Adding a book to a non-existent author → 404.
  • GET /authors/1/books lists that author's books.
  • GET /books/top returns books with rating ≥ 4, highest first.
  • rating outside 1–5 → 400 (validation).
  • Two service unit tests, including the "author not found" case.

Task breakdown (fewer hand-holds — you fill in the how):

  1. Starter ritual. Two entities this time: Author and Book.
  2. Model the relationship: Book gets @ManyToOne to Author; Author optionally gets @OneToMany(mappedBy="author"). Decide who owns the foreign key (the many side — Book).
  3. Two repositories. On BookRepository, derived queries for "by author" and "rating ≥ 4 ordered desc".
  4. Two services. BookService.addToAuthor(authorId, ...) must fetch the author (404 if missing) before saving the book — a cross-entity rule that lives in the service.
  5. DTOs that avoid infinite JSON loops (a Book serialising its Author serialising its Books…). Return a BookResponse with just authorName, not the whole Author object.
  6. Controllers: nest book routes under authors where it reads naturally; keep /books/top flat.
  7. Verify with curl + H2 console (you'll see a foreign-key column on the books table). Write the tests.
The one genuinely new thing: a relationship is still just fields and a foreign key. The layer map does not change — you have two of each kind of class now, and one service reaches to another entity's repository through its own service. Everything else you already know.
Hint — the infinite-JSON trap (everyone hits this once)
If GET /authors/1/books throws a serialization/stack-overflow error, it's the bidirectional relationship serialising in a loop. Fix by returning DTOs (recommended — a BookResponse(id, title, year, rating, authorName)), or by annotating the back-reference with @JsonIgnore. DTOs are the cleaner, more real-world answer.

Capstone 3 · many-to-manyOrder Management — Online Grocery#

A real take-home shape: customers, grocery items, and orders that tie them together. The new muscle is the many-to-many via a join entity from Section 2, plus a derived total. Pure data management — no auth — so you can focus on the relationships.

Spec you build

What the Grocery API must do#

Entities. Customer (name, email, address, phone) · GroceryItem (name, category, price, quantity) · Order (customer, one-or-more items with quantities, orderDate, totalPrice). A customer places many orders; an order contains many items; an item appears in many orders.

  • Full CRUD for Customer, GroceryItem, and Order.
  • Creating an order references an existing customer and existing items (with a quantity each).
  • totalPrice is computed by the server from line items × price, never sent by the client.
  • Referencing a missing customer/item/order returns 404 via @ControllerAdvice.
  • Invalid input (blank name, negative price/quantity) returns 400.

Feature / task breakdown (medium scaffold — you fill the how):

  1. Blueprint it (Chapter 7). Note the join: Order ↔ GroceryItem needs a quantity, so use an OrderItem join entity (Section 2), not a plain @ManyToMany.
  2. Starter ritual. Entities: Customer, GroceryItem, Order, OrderItem. A Category enum if you like.
  3. Build the two simple entities first, end-to-end: Customer CRUD, then GroceryItem CRUD. Get them green before touching orders.
  4. Repositories for all four (JpaRepository). Add validation (@NotBlank, @Positive) on the create DTOs.
  5. Model the order: Order with @ManyToOne Customer and @OneToMany OrderItem; OrderItem with two @ManyToOnes + quantity.
  6. OrderService.create: load the customer (404 if missing), load each item (404 if missing), build the OrderItem lines, compute totalPrice, set orderDate, save.
  7. Return a DTO (OrderResponse) with customer name + a list of {itemName, quantity, lineTotal} + total — avoid serialising entity graphs in a loop (Capstone 2's trap).
  8. Add the GlobalExceptionHandler (Section 2) so all 404/400s are uniform.
  9. Verify each route with curl/Postman; H2 console shows a join table row per line. Optional: a couple of service unit tests (total is correct; missing item → 404).
Definition of done: CRUD works for all three, an order computes its own total from real line items, and every bad reference/input returns the right code through one advisor. That's the whole assignment.
Hint — the most common Grocery mistake
Trusting the client's totalPrice, or storing items as a plain @ManyToMany and then having nowhere to put quantity. Both are fixed by the join entity + computing the total in the service. Re-read Section 2 if either bites.

Capstone 4 · Basic AuthRentVideo#

Now security enters. Same CRUD muscles, plus the Chapter 11 layer: register/login, roles, and endpoints that only an ADMIN may reach. Persist to MySQL if you want the full take-home experience (Chapter 4's swap).

Spec you build

What RentVideo must do#

Users register with email, password (BCrypt-hashed), first/last name, and a role that defaults to CUSTOMER (or ADMIN). Videos have title, director, genre, availability. Public: register/login. Private: browsing videos (any logged-in user). ADMIN-only: create/update/delete videos. Auth via Basic Auth.

  • Register hashes the password; role defaults to CUSTOMER.
  • Login works via Basic Auth (email:password).
  • Any authenticated user can GET /videos; an anonymous request gets 401.
  • A CUSTOMER creating/deleting a video gets 403; an ADMIN succeeds.
  • Errors are uniform 404/400/401/403.

Task breakdown (leans on Chapter 11):

  1. Blueprint. Entities: AppUser (+ Role enum), Video. Decide which endpoints are public / user / admin (Exercise 11.1 is exactly this).
  2. Starter ritual + add spring-boot-starter-security and spring-boot-starter-validation.
  3. Build Video CRUD first, unsecured, and get it green (reuse the TaskFlow shape).
  4. Add the Chapter 11 security stack: AppUser/AppUserRepository, UserDetailsService, SecurityConfig (PasswordEncoder + filter chain), AuthService.register.
  5. Write the authorizeHttpRequests rules: /auth/register public, GET videos for any role, write verbs ADMIN-only (mind the ordering trap).
  6. Global exception handler for uniform errors.
  7. Verify with Postman: register a CUSTOMER and an ADMIN, prove the 401/403/200 matrix. Optional: a @WithMockUser test per rule (Chapter 11, Section 6).
Definition of done: the public/user/admin matrix behaves exactly as specified, and no password is ever stored in plaintext (check the H2/MySQL users table — you should see $2a$ hashes).

Capstone 5 · JWT + rulesRentVideo Advanced#

The summit. RentVideo, but with JWT stateless auth (Chapter 12) and a genuine business rule: rentals with a per-user limit. Several design choices are deliberately left to you — be ready to defend them, as the real brief asks.

Spec you build & decide

What RentVideo Advanced adds#

Everything in RentVideo, but auth is JWT (login returns a token; requests carry Authorization: Bearer). Plus Rentals: a user can rent a video, return it, and may hold at most two active rentals at once. Endpoints include POST /videos/{id}/rent and POST /videos/{id}/return; design the rest.

  • Login returns a JWT; protected endpoints require a valid Bearer token (Chapter 12).
  • A user renting a 3rd active video gets a clear 409 (or 400), not a 500.
  • Returning a video frees a slot; the user can rent again.
  • You documented your design choices (who can rent? schema for rentals?) in the README, ready to defend.

Task breakdown (light scaffold — you own the design):

  1. Start from your RentVideo (Capstone 4). Swap Basic Auth for the Chapter 12 JWT stack: JwtService, JwtAuthFilter, stateless SecurityConfig, /auth/login returning a token.
  2. Model Rental (which user, which video, returned?, timestamps). Decide the schema and defend it.
  3. RentalService.rent: identify the user from the security context, enforce the "< 2 active" rule (throw a 409 exception handled by your advisor), save the rental. (Exercise 10.1 is the core of this.)
  4. RentalService.return: mark the user's active rental for that video as returned.
  5. Wire the two endpoints; keep rules in the service, identity from the token, storage in the repository.
  6. Verify the full flow with Postman: login → rent two → third is rejected → return one → rent succeeds.
Definition of done: a stateless token guards everything, the two-rental rule holds under a real sequence of calls, and you can explain every design choice you made. If you can do this, you can build a secured Spring Boot service from a spec — full stop.

Capstone 6 · scaffold offYour own app#

Open you spec & build

Pick a domain you actually care about#

Choose something small from your life: a habit tracker, an expense log, a recipe box, a workout diary, a movie watchlist. Write the spec yourself, then build it with no breakdown provided. You write the tasks now.

Constraints to keep it a real exercise (and finishable):

  • At least one entity with 4+ fields, full CRUD, and validation on at least one field.
  • At least two derived queries (a filter and a sort/search).
  • Correct 404 on missing ids; correct 400 on bad input.
  • Layers kept clean: no rules in controllers, no SQL/HTTP leaking across seams.
  • At least three service unit tests.
  • Bonus: a second entity with a relationship (like Capstone 2).

Before you code, fill the Chapter 7 blueprint template and write your own task breakdown in the T1/T2/T3 style above. That planning step — turning a spec into a blueprint and an ordered task list — is now yours to own. When it's done, you have a portfolio project and, more importantly, proof you can start from nothing.


Section 3When you want more projects: get a breakdown on demand#

You mentioned wanting features/tasks broken down for you. You can generate exactly this whenever you like. The trick is asking for the breakdown, not the code — so you still do the building.

a prompt that gives you a Chapter-7-style task list, not a solution
I'm practicing Spring Boot (controller/service/repository/JPA/H2, the
layered style). Give me a build-from-scratch spec for a<DOMAIN> API:
- one or two entities with fields
- 6-8 acceptance criteria (CRUD + a filter + validation + 404 cases)
- an ordered feature/task breakdown (T1, T2, ...) naming which layer each
  task touches
Do NOT write the Java for me. Only the spec, the acceptance checks, and the
task list. I'll implement it and can ask for a hint on a specific task if
I'm stuck.
// keep the learning yours

Ask for specs and task breakdowns freely; ask for hints on a specific task when stuck; save asking for full code for after you've attempted and want to compare. The moment the assistant writes the layers for you, the rep is spent. This whole course is built so you don't need that — the shape is in your hands now.


Section 4Self-review rubric — grade your own build#

Run this over any capstone. It's also exactly what a code reviewer checks, so it doubles as interview prep.

CheckGreen if…Smell if…
Layer purityControllers are one-line calls; rules only in services; only repositories touch the DB.An if business rule or a query sits in a controller.
InjectionNo new for services/repos; dependencies come through constructors.You new'd a service, or used field @Autowired without reason.
ErrorsMissing id → 404, bad input → 400, both meaningful.Every failure is a 500, or a null slips back to the client.
DTO boundaryRequest/response shapes are DTOs; entities aren't dictated by clients.Clients set ids/timestamps; entities serialise in loops.
TestsService rules covered by fast Mockito tests; they'd catch a broken rule.No tests, or tests that boot the whole app to check one method.
NamingMethods read like sentences; derived queries do the querying.Hand-written SQL where a derived name would do; vague method names.

Where you started, where you are#

the course, closed

  • You began unsure how controllers, services, repositories, JPA, Lombok, and Mockito related — they felt like a bag of unrelated words.
  • Now each one has a home on one assembly line, and you can read any Spring class with a fixed four-question routine.
  • You built the same project up through every layer, swapped its database without touching the layers above, and tested it in isolation.
  • You practiced with fading instructions, then built from a spec with no code given.

Keep this true#

  • ⭐New term? Ask "which seam?" before "what is it?"
  • ⭐New feature? Triage the layer before you type; build one vertical slice at a time.
  • ⭐Stuck? You've almost certainly built this shape before — go find it and adapt.
  • 💡Come back to Chapter 1, The Layer Map whenever a new project feels like a pile of files. It won't be — it's always the same line.
// go deeper

For the concepts underneath what you just built — the theory of IoC/DI, the anatomy of a REST request, the full test-double taxonomy, JPA relationships in depth, and MongoDB — read the companion Spring Boot Framework collection. You now have the hands-on frame to hang all of it on.

Capstone handoff#

Current state: an Applied TaskFlow sprint. Outcome: an independent capstone plan. Prerequisite: the synthesis checklist, available to revisit at any time. Java bridge: the same interfaces and tests support a new domain. Estimate: 90 minutes.

Mission: choose an independent domain and prove one slice#

Pick one capstone, create its own plan, and implement one vertical slice. This page deliberately does not turn SpendLog, Grocery, or RentVideo into TaskFlow.

  • The capstone has its own domain names and acceptance checks.
  • One endpoint is proven with curl or a test.
  • The layer and ownership decisions have written reasons.

Likely files: your new project’s controller, service, repository, model, tests, and README. Suggested commit: feat: start independent spring capstone.

Hint 1
Reuse the method, not the TaskFlow class names.
Hint 2
Start with one vertical slice and its proof.
Hint 3
Keep a short decision log for every boundary choice.

Before opening the solution: write the domain rule, acceptance proof, files, and first commit yourself.

Reference patch — capstone plan
A good plan names the user-visible request, its data and rule, the layers involved, and the test or curl proof. It does not supply the implementation.

Recall before continuing#

  1. What transfers from TaskFlow to a new domain?
  2. What must be redesigned for the new domain?
  3. How will you prove the first slice?

This is the end of the guided Learn path: continue by building an independent app, then return to the roadmap for focused review.