Study Notes
Spring Boot by Buildingcapstone

TaskFlow Integration Sprint

Fading-scaffold practice on the existing TaskFlow project: clear checks first, then vaguer missions that force layer triage, recall, and independent implementation.

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

After this you should be able to

  • Integrate Spring Boot skills in one application
  • Diagnose layer-level issues
PracticeTaskFlowHints

Spring Boot by Building · Chapter 13

You've watched every layer get built, tested, logged, designed, and secured. Now you build, on the TaskFlow project you already have. The exercises begin with clear steps and get deliberately vaguer — because "I can't start without exact instructions" is the exact gap this chapter closes. For the vaguer ones, sketch a mini blueprint (Chapter 7) first. Hints are a ladder; solutions are a click away, for after you've tried.

Chapter 13 · You do — fading scaffold · Continues the TaskFlow project · ~2–3 hrs across sittings


Choose your route#

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

Learning goal
  • Integrate Spring Boot skills in one application
  • Diagnose layer-level issues

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 integrate spring boot skills in one application as the base, then plan an independent spring boot build in Build from scratch. Interview next: Use integrate spring boot skills in one application as the base, then plan an independent spring boot build in Build from scratch.

Continue to Build from scratch

// how to use this chapter (read this first)

For each exercise: (1) read the goal and the acceptance checks; (2) before touching the keyboard, say out loud which layers change — that triage is the skill; (3) attempt it; (4) if stuck for more than ~10 minutes, open Hint 1, not the solution. The solutions exist so you never rage-quit — but every hint you resist is a rep. Do them in order; each reuses the one before.

// the method for getting unstuck (use it every time)

When you don't know where to start, run this: (a) What layer owns this? (URL→controller, rule→service, field→model+repo, storage→config). (b) Have I done something with this shape before? (almost always yes — find it in Chapters 2–5). (c) Write the smallest version that could work, run it, then grow it. Blank-page paralysis is nearly always a missing step (a); name the layer and the page stops being blank.

Set A · scaffold highWarm-ups with clear steps#

Exercise 6.1 you do

Reject a blank title with a 400#

Goal. POST /tasks with an empty or missing title returns HTTP 400 (Bad Request), not a saved task with a blank title.

  • POST {"title":"","done":false} → 400.
  • POST {"title":"Real task"} → 200 and it saves.

Steps (clear):

  1. Add the validation starter

    Add spring-boot-starter-validation to pom.xml and reload.

  2. Constrain the DTO field

    Put @NotBlank on the request's title.

  3. Turn validation on at the controller

    Add @Valid before @RequestBody.

Hint — a record field can't take @NotBlank directly the usual way
On a record, annotate the component: public record TaskRequest(@NotBlank String title, boolean done) {}. Import jakarta.validation.constraints.NotBlank.
Show solution
dto/TaskRequest.java
import jakarta.validation.constraints.NotBlank;

public record TaskRequest(@NotBlank String title, boolean done) { }
TaskController.java — create()
import jakarta.validation.Valid;

@PostMapping
public Task create(@Valid @RequestBody TaskRequest req) {
    return service.create(req.title());
}

Spring now returns 400 automatically when @Valid fails, before your method runs. The rule lives declaratively on the DTO — no if (title.isBlank()) scattered around.

Exercise 6.2 you do

Add a priority field, end to end#

Goal. A task has a priority (an int, default 3). You can set it on create/update, and it comes back in every response.

  • POST with "priority": 1 saves and returns it.
  • POST without priority defaults to 3.
  • The H2 TASKS table has a priority column.

Which layers? (named for you this time — the fade begins) Model (new field), DTO (accept it), Service (pass it through, apply the default), Controller (no change if the DTO carries it). The repository is untouched — JPA adds the column automatically.

Hint 1 — the default
Decide where "default 3" lives. Cleanest: in the service's create, if the incoming priority is 0/absent, use 3. Or give the DTO a sensible value. Pick one home for the rule — don't put it in two places.
Hint 2 — the DDL
With ddl-auto=update, adding the field to the entity makes Hibernate add the column on next startup. Check the startup SQL log to confirm.
Show solution
model/Task.java
private int priority;   // add the field (+ Lombok generates the accessor)
dto/TaskRequest.java
public record TaskRequest(@NotBlank String title, boolean done, Integer priority) { }
TaskService.java — create()
public Task create(String title, Integer priority) {
    Task t = new Task();
    t.setTitle(title);
    t.setDone(false);
    t.setPriority(priority == null ? 3 : priority);   // the default rule, one home
    return repository.save(t);
}

Update the controller's create to pass req.priority(). Do the same for update. The point: a new field is a predictable walk down the same layers, every time.


Set B · scaffold lowerGoal + hints, no step list#

From here you get the goal, the acceptance checks, and hints — but no numbered steps. Do the triage yourself.

Exercise 6.3 you do

Return only unfinished tasks, highest priority first#

Goal. GET /tasks/open returns tasks where done=false, sorted by priority ascending (1 = most urgent, first).

  • Only not-done tasks appear.
  • They're ordered by priority, 1 before 3.
  • The sorting/filtering is a repository or service concern, not done in the controller.
Hint 1
Spring Data can do both in one derived-query name. Recall Exercise 4.1's findByDone; you can extend the grammar with OrderBy…Asc.
Hint 2 — the method name
findByDoneFalseOrderByPriorityAsc(). Read it as a sentence: find where done is false, order by priority ascending. No body, no SQL.
Show solution
TaskRepository.java
List<Task> findByDoneFalseOrderByPriorityAsc();
service + controller
// service
public List<Task> openByPriority() { return repository.findByDoneFalseOrderByPriorityAsc(); }
// controller
@GetMapping("/open")
public List<Task> open() { return service.openByPriority(); }
Exercise 6.4 you do

Paginate the task list#

Goal. GET /tasks?page=0&size=5 returns at most 5 tasks and metadata about total pages.

  • With 12 tasks, page=0&size=5 returns 5; page=2 returns 2.
  • The response includes total element/page counts.
Hint 1
JpaRepository already has findAll(Pageable) returning a Page<Task>. You barely write anything — mostly plumb Pageable down from the controller.
Hint 2 — the controller signature
Accept a Pageable parameter directly; Spring builds it from ?page=&size=&sort=. Return the Page<Task> — Jackson serialises its metadata for you.
Show solution
service + controller
// service
public Page<Task> page(Pageable pageable) { return repository.findAll(pageable); }
// controller  (import org.springframework.data.domain.*)
@GetMapping("/paged")
public Page<Task> paged(Pageable pageable) { return service.page(pageable); }

Notice how little you wrote: pagination is a built-in seam. Recognising "the framework probably already does this" is a real skill — check before you build.


Set C · scaffold minimalJust a goal — you design the rest#

These are the "not so clear instructions" you asked for. A goal, acceptance checks, and one nudge. You decide the endpoints, the layers, the method names. This is what a real ticket looks like.

Exercise 6.5 you do

Search tasks by title#

Goal. A user can find tasks whose title contains a piece of text, case-insensitively.

  • Searching "milk" finds "Buy milk" and "MILK run".
  • Searching something absent returns an empty list, not an error.
  • You chose a sensible URL and put the query at the repository seam.
One nudge
Derived-query grammar again: findByTitleContainingIgnoreCase(String q). You design the endpoint (e.g. GET /tasks/search?q=milk) and wire the two layers above it.
Show one correct shape
// repository
List<Task> findByTitleContainingIgnoreCase(String q);
// service
public List<Task> search(String q) { return repository.findByTitleContainingIgnoreCase(q); }
// controller
@GetMapping("/search")
public List<Task> search(@RequestParam String q) { return service.search(q); }

Yours may differ in naming — that's fine. If the checks pass and each concern sits in the right layer, it's correct. There's rarely one right answer, only right shapes.

Exercise 6.6 you do

Give tasks a due date and flag overdue ones#

Goal. A task can have a dueDate. There's a way to list all tasks that are past due and still not done.

  • You can create a task with a due date and read it back.
  • An endpoint returns only tasks with dueDate < today and done=false.
  • Tasks with no due date never count as overdue.
One nudge
Use java.time.LocalDate for the field — JPA maps it to a DATE column with no fuss. For the query, a derived name like findByDoneFalseAndDueDateBefore(LocalDate date) works; pass LocalDate.now() from the service.
Show one correct shape
// model: private LocalDate dueDate;   (nullable — no annotation needed)
// repository
List<Task> findByDoneFalseAndDueDateBefore(LocalDate date);
// service
public List<Task> overdue() { return repository.findByDoneFalseAndDueDateBefore(LocalDate.now()); }
// controller
@GetMapping("/overdue")
public List<Task> overdue() { return service.overdue(); }

Null due dates are excluded automatically — DueDateBefore can't match a null, so the third check passes for free. Add the field to the DTO/create path too so you can set it.

// bonus reps if you want them

Write a unit test (Chapter 5 style) for each new service method here. Add @Valid constraints where sensible (a priority should be 1–5: @Min(1) @Max(5)). Every one is the same walk down the same layers — the repetition is the point; it's how the shape becomes automatic.


Notebook Summary#

The practice habits

Before typing, always#

  • Name the layer(s): URL→controller · rule→service · field→model+repo · storage→config
  • Find a past exercise with the same shape and adapt it
  • Build the smallest version, run it, then grow

Patterns you now own#

  • Validation: @NotBlank/@Min on the DTO + @Valid at the controller
  • New field: model → DTO → service (defaults) → controller
  • Derived queries: findByDoneFalseOrderByPriorityAsc, …ContainingIgnoreCase, …DueDateBefore
  • Built-ins first: Pageable/Page before hand-rolling

Box these#

  • ⭐Blank-page paralysis = a skipped triage step. Name the layer; the page fills.
  • ⭐Resist the solution. A hint you didn't need to open is a rep you banked.
  • 💡There's rarely one right answer — only right shapes. Passing checks + right layers = done.
// next — the scaffold comes off

Chapter 14 — Build from scratch. No TaskFlow to lean on. You get a product spec and a task breakdown for a brand-new app, an empty start.spring.io, and a checklist — and you'll blueprint each one first with the Chapter 7 method. This is where "I can't write it myself" turns into "I just did."

TaskFlow Applied sprint#

Current state: TaskFlow has layered persistence, boundary validation, tests, logging, and authorization. Outcome: one fresh requirement change spanning them. Prerequisite: you may revisit prior chapters; this is synthesis, not a lock. Java bridge: composition lets each concern remain testable. Estimate: 60 minutes.

Mission: add a due-date reminder policy end to end#

Add a validated due date and reminder preference to a task, expose it through DTOs, persist it, enforce an owner rule, log the decision, and test both the allowed and rejected cases.

  • A controller validates the new request field and response DTO exposes the allowed result.
  • The service applies the reminder rule inside the owner-scoped flow.
  • The repository persists the change, tests cover the rule, and logs identify the task without secrets.
  • A bearer-token curl request proves the authorized path and a cross-owner request is denied.

Likely files: controller/TaskController.java, dto/TaskRequest.java, service/TaskService.java, repository/TaskRepository.java, model/Task.java, relevant tests and logging configuration. Suggested commit: feat: add taskflow reminder policy.

Hint 1
Trace the new field from request DTO to entity to response DTO.
Hint 2
Keep the policy in the service, not in the controller or repository.
Hint 3
Write the rejected owner case before the happy path.

Before opening the solution: create a requirement checklist for controller, service, repository, validation, tests, logging, and authorization; run every proof yourself.

Reference patch — sprint shape
The change crosses every seam, but each layer still owns only its normal responsibility. The proof is curl plus tests plus a deliberate log trace.

Recall before continuing#

  1. Where does the reminder rule live?
  2. Which test proves authorization?
  3. What makes the log useful but safe?

The independent capstone follows only after this integrated proof, so you can carry the method rather than copy TaskFlow.