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.
Assumes you know
After this you should be able to
- Integrate Spring Boot skills in one application
- Diagnose layer-level issues
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.
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.
- 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.
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.
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#
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):
- Add the validation starter
Add
spring-boot-starter-validationtopom.xmland reload. - Constrain the DTO field
Put
@NotBlankon the request'stitle. - Turn validation on at the controller
Add
@Validbefore@RequestBody.
Hint — a record field can't take @NotBlank directly the usual way
record, annotate the component: public record TaskRequest(@NotBlank
String title, boolean done) {}. Import jakarta.validation.constraints.NotBlank.Show solution
import jakarta.validation.constraints.NotBlank;
public record TaskRequest(@NotBlank String title, boolean done) { }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.
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": 1saves and returns it. - POST without priority defaults to 3.
- The H2
TASKStable has aprioritycolumn.
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
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
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
private int priority; // add the field (+ Lombok generates the accessor)public record TaskRequest(@NotBlank String title, boolean done, Integer priority) { }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.
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
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
List<Task> findByDoneFalseOrderByPriorityAsc();// service
public List<Task> openByPriority() { return repository.findByDoneFalseOrderByPriorityAsc(); }
// controller
@GetMapping("/open")
public List<Task> open() { return service.openByPriority(); }Goal. GET /tasks?page=0&size=5 returns at most 5 tasks and
metadata about total pages.
- With 12 tasks,
page=0&size=5returns 5;page=2returns 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
Pageable parameter directly; Spring builds it from
?page=&size=&sort=. Return the Page<Task> — Jackson serialises its
metadata for you.Show solution
// 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.
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
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.
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 < todayanddone=false. - Tasks with no due date never count as overdue.
One nudge
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.
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/@Minon the DTO +@Validat the controller - New field: model → DTO → service (defaults) → controller
- Derived queries:
findByDoneFalseOrderByPriorityAsc,…ContainingIgnoreCase,…DueDateBefore - Built-ins first:
Pageable/Pagebefore 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.
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
Hint 2
Hint 3
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
Recall before continuing#
- Where does the reminder rule live?
- Which test proves authorization?
- 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.