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.
Assumes you know
After this you should be able to
- Plan an independent Spring Boot build
- Demonstrate transferable Spring skills
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.
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.
- 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.
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.
- 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.
- 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.)
- Configure
application.properties: the H2 block from Chapter 4 +show-sql=true+h2.console.enabled=true. Do this before writing code. - Make the packages
controller,service,repository,model,dtounder your base package. Empty folders, ready to fill. - 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.
- Verify & test
curl / H2 console for a manual pass; one Mockito service test per rule. Then move to the next task.
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.
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()));
}
}- 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
@ResponseStatusannotations (or keep them — an explicit@ExceptionHandlerhere 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
Join entity (recommended here)#
- An
OrderItemrow = one line of an order - Holds quantity, unit price — real data
- Two
@ManyToOnes replace one@ManyToMany
@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
}- 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. totalPriceis 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.
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.
createdAtis 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:
- Blueprint it with the Chapter 7 template, then run the starter ritual (Section 1). Project
artifact:
bookmarks. - Model:
Bookmarkentity with the six fields.@Id @GeneratedValueon id;@Column(nullable=false)on url; Lombok@Getter/@Setter/@NoArgsConstructor. - Repository:
interface BookmarkRepository extends JpaRepository<Bookmark, Long>. Add derived queriesfindByTag(String)andfindByFavoriteTrue(). - Service: methods
getAll,getById(throws 404),create,update,delete,byTag,favorites. SetcreatedAt = LocalDateTime.now()insidecreate. - 404 type: a
BookmarkNotFoundExceptionwith@ResponseStatus(NOT_FOUND)(copy the pattern from Chapter 3). - DTO:
BookmarkRequestrecord with@NotBlank url, plus title, tag, favorite. No id, no createdAt — the server owns those. - Controller:
@RequestMapping("/bookmarks")+ the CRUD routes, plusGET /bookmarks?tag=workandGET /bookmarks/favorites. Use@Valid. - Verify by hand: curl each route; open the H2 console and confirm the table + a row.
- Test:
BookmarkServiceTest—createsets createdAt & favorite defaults;getByIdmissing throws. Mock the repository.
./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.
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/bookslists that author's books.GET /books/topreturns books with rating ≥ 4, highest first.ratingoutside 1–5 → 400 (validation).- Two service unit tests, including the "author not found" case.
Task breakdown (fewer hand-holds — you fill in the how):
- Starter ritual. Two entities this time:
AuthorandBook. - Model the relationship:
Bookgets@ManyToOnetoAuthor;Authoroptionally gets@OneToMany(mappedBy="author"). Decide who owns the foreign key (the many side — Book). - Two repositories. On
BookRepository, derived queries for "by author" and "rating ≥ 4 ordered desc". - 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. - DTOs that avoid infinite JSON loops (a Book serialising its Author serialising its Books…). Return a
BookResponsewith justauthorName, not the whole Author object. - Controllers: nest book routes under authors where it reads naturally; keep
/books/topflat. - Verify with curl + H2 console (you'll see a foreign-key column on the books table). Write the tests.
Hint — the infinite-JSON trap (everyone hits this once)
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.
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).
totalPriceis 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):
- Blueprint it (Chapter 7). Note the join: Order ↔ GroceryItem needs a quantity, so use an
OrderItemjoin entity (Section 2), not a plain@ManyToMany. - Starter ritual. Entities:
Customer,GroceryItem,Order,OrderItem. ACategoryenum if you like. - Build the two simple entities first, end-to-end: Customer CRUD, then GroceryItem CRUD. Get them green before touching orders.
- Repositories for all four (JpaRepository). Add validation (
@NotBlank,@Positive) on the create DTOs. - Model the order:
Orderwith@ManyToOne Customerand@OneToMany OrderItem;OrderItemwith two@ManyToOnes + quantity. - OrderService.create: load the customer (404 if missing), load each item (404 if missing),
build the
OrderItemlines, compute totalPrice, set orderDate, save. - 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). - Add the
GlobalExceptionHandler(Section 2) so all 404/400s are uniform. - 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).
Hint — the most common Grocery mistake
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).
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):
- Blueprint. Entities:
AppUser(+Roleenum),Video. Decide which endpoints are public / user / admin (Exercise 11.1 is exactly this). - Starter ritual + add
spring-boot-starter-securityandspring-boot-starter-validation. - Build Video CRUD first, unsecured, and get it green (reuse the TaskFlow shape).
- Add the Chapter 11 security stack:
AppUser/AppUserRepository,UserDetailsService,SecurityConfig(PasswordEncoder+ filter chain),AuthService.register. - Write the
authorizeHttpRequestsrules:/auth/registerpublic, GET videos for any role, write verbs ADMIN-only (mind the ordering trap). - Global exception handler for uniform errors.
- Verify with Postman: register a CUSTOMER and an ADMIN, prove the 401/403/200 matrix. Optional: a
@WithMockUsertest per rule (Chapter 11, Section 6).
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.
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):
- Start from your RentVideo (Capstone 4). Swap Basic Auth for the Chapter 12 JWT stack:
JwtService,JwtAuthFilter, statelessSecurityConfig,/auth/loginreturning a token. - Model
Rental(which user, which video, returned?, timestamps). Decide the schema and defend it. - 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.)
- RentalService.return: mark the user's active rental for that video as returned.
- Wire the two endpoints; keep rules in the service, identity from the token, storage in the repository.
- Verify the full flow with Postman: login → rent two → third is rejected → return one → rent succeeds.
Capstone 6 · scaffold offYour own app#
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.
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.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.
| Check | Green if… | Smell if… |
|---|---|---|
| Layer purity | Controllers are one-line calls; rules only in services; only repositories touch the DB. | An if business rule or a query sits in a controller. |
| Injection | No new for services/repos; dependencies come through
constructors. | You new'd a service, or used field @Autowired without
reason. |
| Errors | Missing id → 404, bad input → 400, both meaningful. | Every failure is a 500, or a null slips back to the client. |
| DTO boundary | Request/response shapes are DTOs; entities aren't dictated by clients. | Clients set ids/timestamps; entities serialise in loops. |
| Tests | Service 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. |
| Naming | Methods 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.
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
Hint 2
Hint 3
Before opening the solution: write the domain rule, acceptance proof, files, and first commit yourself.
Reference patch — capstone plan
Recall before continuing#
- What transfers from TaskFlow to a new domain?
- What must be redesigned for the new domain?
- 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.