Spring Boot by Building · Chapter 7

From requirements to a blueprint

The question you actually asked: given a requirements sheet, what do I make, and how do I decide? This chapter is a repeatable cycle — nouns to entities, verbs to operations, responsibilities to layers, and the real rules for when to reach for an interface, an abstract class, or just a class. You leave with a template you run on every new project.

Chapter 7 · The design method · A thinking chapter · Read ~16 min


Choose your route

Study the same From requirements to a blueprint explanation with a different purpose: build and verify it in Learn deeply, or retrieve and defend it in Prepare for interviews.

Learning goal

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.

// the big idea

Good design is not inspiration — it's a procedure. You extract the things and the actions from the spec, drop each onto the layer that owns it, decide the handful of places abstraction earns its keep, and sketch a one-page blueprint before you type. This is what people mean by LLD (low-level design), done at a beginner-friendly pace. Run the cycle enough times and "where do I start?" answers itself.

// why this removes the paralysis

Blank-page fear comes from trying to hold the whole app in your head at once. The cycle replaces that with small, ordered decisions, each with a rule. You never face "design the app" — only "list the nouns," then "list the verbs," then "which layer?" Each step is answerable, and their sum is the design.

Section 1The design cycle — five steps, every time

1 · Nouns → data→ 2 · Verbs → operations→ 3 · Assign to layers→ 4 · Decide abstractions→ 5 · Blueprint & sequence

We'll define each step, then run the whole cycle on a real spec in Section 5. Keep a spec in mind as you read — here's the running example:

// running example spec — "SpendLog"

"Users track expenses. Each expense has an amount, a category (food, travel, bills…), a date, and a note. They can add, edit, delete, and list expenses, filter by category, and get a monthly total. When an expense over a configurable limit is added, the app should send the user an alert — by email now, maybe SMS later."


Section 2Steps 1 & 2 — nouns become data, verbs become operations

The oldest trick in object design, and it genuinely works: underline the nouns and the verbs.

Nouns → candidate models & fields

expense (the thing) → an Expense entity. amount, category, date, note → its fields. user → maybe an entity later; limit → a config value, not an entity.

Verbs → operations (service + endpoints)

add, edit, delete, list, filter by category, monthly total, send alert → each becomes a service method, and the user-facing ones become controller routes.

// how to tell an entity from a mere field or a config value

A noun is an entity if it has its own identity and you'd store many of them (expenses). It's a field if it only describes another thing (an expense's amount). It's config if it's one tunable value for the whole app (the limit). Ask: "would I ever need a list of these, each with its own id?" Yes → entity. No → field or config.

After steps 1–2 you have two lists. For SpendLog:

NounVerdictVerbBecomes
expenseEntity Expenseadd expensecreate() + POST
amount, category, date, noteFields of Expenseedit / deleteupdate()/delete() + PUT/DELETE
categoryField (an enum)list / filter by categorygetAll(category) + GET
monthly totalDerived value, not storedmonthly totalmonthlyTotal() + GET
limitConfig valuesend alertan AlertService call (see step 4)

Section 3Step 3 — drop each operation onto its layer

Now the layer map from Chapter 1 does the sorting. Every operation lands somewhere predictable:

// the payoff of doing this on paper first

By the end of step 3 you can already name every class you'll create and roughly its methods — without a line of code. That list is your task breakdown (Chapter 6/9 style). Design and planning turn out to be the same activity.


Section 4Step 4 — when to reach for an interface or abstract class

This is the part that feels like dark art. It isn't. There are three tools and a clear rule for each. The default is the simplest one.

ToolReach for it when…Beginner rule
Concrete class There's exactly one way to do the job and no reason to swap it. The default. Most services and controllers are just classes. Don't abstract until you need to.
Interface (+ one or more impls) There are — or will plausibly be — multiple ways to do the job, or you cross an external boundary (email, payment, storage) you'll want to fake in tests. "Same question, different answers" → interface. E.g. AlertService with Email + SMS.
Abstract class Several variants share real code, not just a method list — common fields/logic with a few bits that differ. Rare early. Use when impls would copy-paste the same helper; put shared code in the abstract parent.
// pitfall — the "one-impl interface" ceremony

A beginner reflex (from tutorials) is to make TaskService an interface with a single TaskServiceImpl. If there's only ever one implementation and no external boundary, that interface is pure ceremony — extra files, no benefit. Make it a plain class. Add the interface the day a second implementation or a test-fake actually appears. (Repositories are the exception — Spring Data needs the interface, and provides the impl.)

// the SpendLog decision, worked

ExpenseService → concrete class (one way to manage expenses). AlertService → interface, because the spec literally says "email now, maybe SMS later" — that's "multiple answers to the same question," the textbook trigger. So: AlertService (interface) with EmailAlertService today, SmsAlertService tomorrow. The ExpenseService depends on the interface, and Spring injects whichever impl is active. That's polymorphism + DI doing real work — Chapter 8 builds exactly this.

// analogy — power sockets

An interface is a socket: it fixes the shape of the plug (the methods) without caring what's behind the wall. EmailAlertService and SmsAlertService are different appliances that fit the same socket. Your ExpenseService plugs into the socket, not the appliance — so you swap appliances (or plug in a fake for testing) without rewiring the room.


Section 5Step 5 — draw the blueprint (UML-lite)

A blueprint is boxes (classes) and arrows (relationships). You don't need formal UML — just a shared shorthand so the picture is unambiguous. These four relationships cover ~everything you'll draw:

RelationshipMeansArrowExample
"uses / depends on"A calls B (B injected into A)solid arrow → Controller → Service
"is-a" (implements)class fulfils an interfacedashed hollow arrow ⇢▷EmailAlertService ⇢▷ AlertService
"is-a" (extends)class inherits a parentsolid hollow arrow —▷ SmsAlert —▷ AbstractAlert
"has-a"one holds/owns another, with a countline + 1..* Author 1 — * Book
ExpenseController @RestController · routes ExpenseService @Service · rules + total ExpenseRepository JpaRepository «interface» AlertService EmailAlertService SmsAlertService uses uses uses implements

Fig 1 — The SpendLog blueprint. Solid arrows = "uses" (a dependency Spring injects). Dashed hollow arrows = "implements". The service depends on the interface; the two concrete classes fit it.

// what makes a blueprint "good enough to build"

You can start coding when the blueprint answers: every class named, its layer clear, its dependencies drawn, and any interface + its impls identified. That's it. It doesn't need to be pretty or complete — a photo of a whiteboard is a real blueprint. Its job is to make the build a series of obvious steps, and it has done that here.


Section 6The reusable template — copy this for every project

design-blueprint.md — fill one out before you code
## <App name> — blueprint

1. NOUNS → data
   - Entities:        <list, each with fields>
   - Enums:           <list>
   - Config values:   <list — NOT entities>

2. VERBS → operations
   - <verb> → service method <name> (+ endpoint <METHOD /path> if user-facing)

3. LAYERS  (which class owns each operation)
   - Controllers:  <names + routes>
   - Services:     <names + methods; where each rule lives>
   - Repositories: <names + derived queries>
   - Models:       <entities + enums>

4. ABSTRACTIONS  (justify each — default is a plain class)
   - Interface? only if multiple impls OR an external boundary. Name impls.
   - Abstract class? only if impls share real code.

5. BLUEPRINT + BUILD ORDER
   - Boxes & arrows (uses / implements / has-a with counts)
   - Build sequence: one vertical slice at a time, run after each

DEFINITION OF DONE: acceptance checks pass · layers clean · rules single-homed
// this is the answer to "next time I get requirements, where do I start?"

You start at line 1: list the nouns. Then the verbs. The template carries you the rest of the way. You are never designing "an app" — you are filling five short sections, each with a rule. Do this three or four times and you'll run the cycle in your head without the file.


Section 7Exercise — blueprint before code

Exercise 7.1 you do — on paper

Blueprint a "Support Ticket" system

Spec. "Customers open support tickets. A ticket has a subject, a body, a status (open/in-progress/closed), a priority, and a created date. Agents list tickets, filter by status, reassign priority, and close them. When a ticket is closed, notify the customer — email now, in-app later."

  • You produced a filled-in template: entities+fields, enums, the service/controller/repository classes, and derived queries.
  • You justified exactly one interface (hint: the notify requirement).
  • You drew boxes + arrows and a build order.
Hint — the interface decision
"email now, in-app later" = multiple answers to "how do we notify?" → a NotificationService interface with EmailNotificationService now. Everything else (TicketService, TicketController) is a plain class. Status and priority are enums, not entities.
Show a reference blueprint
  • Entities: Ticket (subject, body, status, priority, createdAt). Enums: Status, Priority.
  • TicketRepository (JpaRepository): findByStatus, findByStatusOrderByPriorityAsc.
  • TicketService (class): open, list, byStatus, setPriority, close — and inside close, call the notifier.
  • NotificationService (interface) → EmailNotificationService (impl now). Injected into TicketService.
  • TicketController (class): routes for each user-facing verb.
  • Build order: Ticket + enums → repository → service (minus notify) → controller → get CRUD green → add NotificationService + wire into close → test.

Yours may name things differently — if the five sections are filled and the one interface is justified, it's a good blueprint.


Notebook Summary

The design cycle on one page

The five steps

Abstraction rules

Blueprint arrows

Box these

  • ⭐Design = a procedure, not inspiration. Nouns, verbs, layers, abstractions, blueprint.
  • ⚠Don't make an interface with one impl and no boundary — that's ceremony. Add it when a second answer appears.
  • 💡A finished blueprint is your task breakdown. Design and planning are the same step.
// next

Chapter 8 — Design in action. We run this exact cycle end-to-end on SpendLog and build it — including the AlertService interface with two implementations, wired by DI — so you see the blueprint become working, tested code.

TaskFlow checkpoint

Current state: a working, diagnosable TaskFlow. Outcome: a blueprint before implementation. Prerequisite: the layer seams you have tested. Java bridge: an interface names a variation point. Estimate: 35 minutes.

Mission: turn a requirement into a buildable slice

Produce a boundary map, entity list, and one interface decision for the next change. The artifact is a blueprint, not duplicate explanation of TaskFlow.

  • The request flow has controller, service, repository, and model responsibilities.
  • One interface has a stated reason and a second implementation possibility.
  • The first vertical slice has an observable acceptance check.

Likely files: docs/blueprint.md, later service and repository packages. Suggested commit: docs: blueprint next taskflow slice.

Hint 1
Start from an observable request, not from class names.
Hint 2
Add an interface only at a real variation boundary.
Hint 3
Write a definition of done before implementation.

Before opening the solution: label each responsibility, state one trade-off, and identify the first proof.

Reference patch — blueprint
A small design lists responsibilities, data ownership, a likely seam, and the first request that proves it.

Recall before continuing

  1. What earns an interface?
  2. What makes a vertical slice valuable?
  3. What is the first acceptance proof?

The next workshop applies this design method to an independent app while preserving TaskFlow as the reference shape.