Spring Boot by Building · Chapter 7
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.
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.
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.
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.
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.
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:
"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."
The oldest trick in object design, and it genuinely works: underline the nouns and the verbs.
expense (the thing) → an Expense entity. amount, category, date, note → its
fields. user → maybe an entity later; limit → a config value, not an entity.
add, edit, delete, list, filter by category, monthly total, send alert → each becomes a service method, and the user-facing ones become controller routes.
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:
| Noun | Verdict | Verb | Becomes |
|---|---|---|---|
| expense | Entity Expense | add expense | create() + POST |
| amount, category, date, note | Fields of Expense | edit / delete | update()/delete() + PUT/DELETE |
| category | Field (an enum) | list / filter by category | getAll(category) + GET |
| monthly total | Derived value, not stored | monthly total | monthlyTotal() + GET |
| limit | Config value | send alert | an AlertService call (see step 4) |
Now the layer map from Chapter 1 does the sorting. Every operation lands somewhere predictable:
findByCategory, and a query to sum a month.Expense with its fields; Category as an enum.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.
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.
| Tool | Reach 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. |
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.)
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.
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.
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:
| Relationship | Means | Arrow | Example |
|---|---|---|---|
| "uses / depends on" | A calls B (B injected into A) | solid arrow → | Controller → Service |
| "is-a" (implements) | class fulfils an interface | dashed hollow arrow ⇢▷ | EmailAlertService ⇢▷ AlertService |
| "is-a" (extends) | class inherits a parent | solid hollow arrow —▷ | SmsAlert —▷ AbstractAlert |
| "has-a" | one holds/owns another, with a count | line + 1..* |
Author 1 — * Book |
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.
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.
## <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
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.
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."
NotificationService interface with EmailNotificationService now. Everything else
(TicketService, TicketController) is a plain class. Status and priority are enums, not entities.Ticket (subject, body, status, priority, createdAt).
Enums: Status, Priority.findByStatus,
findByStatusOrderByPriorityAsc.open, list, byStatus,
setPriority, close — and inside close, call the notifier.EmailNotificationService (impl now).
Injected into TicketService.Yours may name things differently — if the five sections are filled and the one interface is justified, it's a good blueprint.
The design cycle on one page
1..*
= has-aChapter 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.
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.
Produce a boundary map, entity list, and one interface decision for the next change. The artifact is a blueprint, not duplicate explanation of TaskFlow.
Likely files: docs/blueprint.md, later service and repository packages. Suggested commit: docs: blueprint next taskflow slice.
Before opening the solution: label each responsibility, state one trade-off, and identify the first proof.
The next workshop applies this design method to an independent app while preserving TaskFlow as the reference shape.