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.
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.
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.
Use this chapter’s TaskFlow work to make one small, observable change. Run the stated acceptance checks before recording practice.
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.
Every new Spring project begins the same way. Internalise this and "getting started" stops being a decision each time — it's a reflex.
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.
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.)
application.properties: the H2 block from Chapter 4 +
show-sql=true + h2.console.enabled=true. Do this before writing code.
controller, service,
repository, model, dto under your base package. Empty folders, ready to
fill.
Model → Repository → Service → Controller. Run after each layer. Get one endpoint green before adding the next feature.
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.
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.
@ControllerAdviceSo 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()));
}
}
@ResponseStatus annotations (or keep them —
an explicit @ExceptionHandler here wins). The specs' "return appropriate HTTP codes" requirement
is satisfied in one place.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.
OrderItem row = one line of an order@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
}
@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.)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.
url → 400.createdAt is set automatically on create, never by the client.Feature / task breakdown — do them in order; run after each:
bookmarks.Bookmark entity with the six fields. @Id @GeneratedValue
on id; @Column(nullable=false) on url; Lombok @Getter/@Setter/@NoArgsConstructor.interface BookmarkRepository extends JpaRepository<Bookmark, Long>.
Add derived queries findByTag(String) and findByFavoriteTrue().getAll, getById (throws 404),
create, update, delete, byTag, favorites. Set
createdAt = LocalDateTime.now() inside create.BookmarkNotFoundException with
@ResponseStatus(NOT_FOUND) (copy the pattern from Chapter 3).BookmarkRequest record with @NotBlank url, plus title, tag,
favorite. No id, no createdAt — the server owns those.@RequestMapping("/bookmarks") + the CRUD routes, plus
GET /bookmarks?tag=work and GET /bookmarks/favorites. Use @Valid.BookmarkServiceTest — create sets createdAt &
favorite defaults; getById missing throws. Mock the repository../mvnw test is green. If yes — you built a Spring Boot service from a spec, alone.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.
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.
POST /authors/1/books).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).Task breakdown (fewer hand-holds — you fill in the how):
Author and Book.Book gets @ManyToOne to Author;
Author optionally gets @OneToMany(mappedBy="author"). Decide who owns the foreign
key (the many side — Book).BookRepository, derived queries for "by author" and "rating ≥ 4 ordered
desc".BookService.addToAuthor(authorId, ...) must fetch the author (404 if missing)
before saving the book — a cross-entity rule that lives in the service.BookResponse with just authorName, not the whole Author object./books/top flat.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.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.
totalPrice is computed by the server from line items × price, never sent by the client.@ControllerAdvice.Feature / task breakdown (medium scaffold — you fill the how):
OrderItem join entity (Section 2), not a plain @ManyToMany.Customer, GroceryItem, Order,
OrderItem. A Category enum if you like.@NotBlank, @Positive)
on the create DTOs.Order with @ManyToOne Customer and
@OneToMany OrderItem; OrderItem with two @ManyToOnes + quantity.OrderItem lines, compute totalPrice, set orderDate, save.OrderResponse) with customer name + a list of {itemName, quantity, lineTotal} +
total — avoid serialising entity graphs in a loop (Capstone 2's trap).GlobalExceptionHandler (Section 2) so all 404/400s are uniform.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.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.
GET /videos; an anonymous request gets 401.Task breakdown (leans on Chapter 11):
AppUser (+ Role enum), Video. Decide which
endpoints are public / user / admin (Exercise 11.1 is exactly this).spring-boot-starter-security and
spring-boot-starter-validation.AppUser/AppUserRepository,
UserDetailsService, SecurityConfig (PasswordEncoder + filter chain),
AuthService.register.authorizeHttpRequests rules: /auth/register public, GET videos for any
role, write verbs ADMIN-only (mind the ordering trap).@WithMockUser test per rule (Chapter 11, Section 6).users table — you should see
$2a$ hashes).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.
Task breakdown (light scaffold — you own the design):
JwtService,
JwtAuthFilter, stateless SecurityConfig, /auth/login returning a token.Rental (which user, which video, returned?, timestamps). Decide the schema and defend it.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):
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.
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.
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. |
the course, closed
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.
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.
Pick one capstone, create its own plan, and implement one vertical slice. This page deliberately does not turn SpendLog, Grocery, or RentVideo into TaskFlow.
Likely files: your new project’s controller, service, repository, model, tests, and README. Suggested commit: feat: start independent spring capstone.
Before opening the solution: write the domain rule, acceptance proof, files, and first commit yourself.
This is the end of the guided Learn path: continue by building an independent app, then return to the roadmap for focused review.