From requirements to a blueprint
Turn plain-English specs into entities, operations, layers, abstractions, and a build order before opening the IDE.
Assumes you know
After this you should be able to
- Turn requirements into a design
- Identify application boundaries
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.
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.
- Turn requirements into a design
- Identify application boundaries
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.
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.
Section 1The design cycle — five steps, every time#
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."
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.
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) |
Section 3Step 3 — drop each operation onto its layer#
Now the layer map from Chapter 1 does the sorting. Every operation lands somewhere predictable:
- Controller — the user-facing verbs (add/edit/delete/list/total) become routes. Thin.
- Service — the rules: "monthly total" is a computation; "alert when over limit" is a decision. This is where logic concentrates.
- Repository — the data access:
findByCategory, and a query to sum a month. - Model —
Expensewith its fields;Categoryas 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.
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.
| 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.
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:
| 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.
Section 6The reusable template — copy this for every project#
## <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-homedYou 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#
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
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 insideclose, 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#
- 1 Nouns → data (entity if it has identity & you'd store many; else field/config)
- 2 Verbs → operations (service methods; user-facing ones also become routes)
- 3 Assign to layers (the layer map sorts every operation)
- 4 Decide abstractions (default = plain class)
- 5 Blueprint + build order (boxes/arrows, one vertical slice at a time)
Abstraction rules#
- Concrete class = default (one way, no swap)
- Interface = multiple impls OR external boundary OR test-fake ("same question, different answers")
- Abstract class = variants share real code
Blueprint arrows#
- solid → = uses/depends · dashed hollow ⇢▷ = implements · solid hollow —▷ = extends · line +
1..*= has-a
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.
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
Hint 2
Hint 3
Before opening the solution: label each responsibility, state one trade-off, and identify the first proof.
Reference patch — blueprint
Recall before continuing#
- What earns an interface?
- What makes a vertical slice valuable?
- What is the first acceptance proof?
The next workshop applies this design method to an independent app while preserving TaskFlow as the reference shape.