Capstone 1: Requirements and Architecture

bee2026-10-0849 min read0 views
BeeOrder begins: turn vague requirements into user stories and acceptance criteria, then fix the domain model, module boundaries, technology choices and project layout.
1 / 157
Section
0. The 30-second version
2 / 157

This article writes no business code at all, yet it decides whether the next three articles are "building upward" or "filling holes". We do exactly one thing: turn a vague wish — "users can place orders" — into a contract nobody has to guess at. The contract states who asks for what (user stories with acceptance criteria), which things and rules exist in this world (the domain model and state machine), where each part stands (layering and module boundaries), and what the system says to the outside world (the API list).

3 / 157

Six terms explained in one line each — they recur throughout:

4 / 157
  • Requirement: the sentence someone says out loud, "I want a checkout feature" — that is not a requirement, it is a wish; only the phrasing you can write a test case against counts
  • User story: the wish rewritten as "As a ⟨role⟩, I want ⟨capability⟩ so that ⟨value⟩", always followed by acceptance criteria you can verify
  • Domain model: the nouns and the iron rules of this business — orders, stock, payments, plus laws like "stock can never go negative" — nothing to do with database tables yet
  • Aggregate root: the single door into a group of objects that must change together. Items are modified through the order, the way an invoice line is corrected by reissuing the invoice
  • Layering: lining your code up by "who stands nearest to the user": Controller first, Service in the middle, Mapper last, with dependencies pointing only forward
  • Contract: a specification both frontend and backend sign, fixing what the request looks like, what the response looks like and which code an error carries. Signed means no unilateral edits
5 / 157
类比|Analogy

think of a restaurant. The waiter (Controller) takes the order, reads out the menu and serves the dish — never storms into the kitchen to wok anything. The head chef (Service) decides "blanch this one first, then stir-fry, and plate it with the cold dish", which is business orchestration and the transaction boundary. The storekeeper (Mapper) only fetches and returns goods against a slip and never decides how much should ship (that is a business rule). Utilities, CCTV and the time clock (infrastructure: Actuator and logging) never appear on the menu, but without them the whole place closes on day one. And when the ticket passes from waiter to chef it gets rewritten: the customer's "kung pao chicken, no spice" becomes kitchen shorthand "kung pao ×1 / no chilli / extra rice" — that gap between the customer's wording (DTO) and the kitchen's wording (entity / VO) is exactly what #44 turns into code.

6 / 157
类比|Analogy

an order snapshot is a used ticket from the register. Today the shop raises the kung pao from 38 to 45 and renames it "Hive Secret Chicken", but your ticket from three months ago must still read 38 and the old name, otherwise month-end reconciliation can never balance. That single rule is why the next article denormalizes order_item with the product name and unit price: it copies the menu, but what it keeps is evidence.

7 / 157
Diagram
Figure · The BeeOrder layer and module map
Figure · The BeeOrder layer and module map
8 / 157

That map is also this article's table of contents: the first branch is the call direction (Section 6), the five after it are what the six business packages own (Section 8), and common at the bottom is the three shared building blocks. While reading, hold on to one fact: no arrow on the map points back to the left — that discipline matters more than any framework detail, and its cost shows up live in the risk list of Section 10 and the error table of Section 13.

9 / 157

After this article you should be able to answer three questions:

10 / 157
  1. Between "checkout should be fast" and "P99 latency under 500ms", why does only the second belong in a requirements document? What is the test?
  2. order may call inventory but not the reverse — why? If someone breaks the rule, does the trouble surface at compile time, at startup or at runtime? The error table in Section 13 gives the answer.
  3. Where is the real line between a monolith and microservices — "how many jars you ship", or "who can scale and release independently"?
11 / 157
Section
1. Project statement: BeeOrder, the Hive Order Center
12 / 157

The previous 40 articles took Spring apart piece by piece — IoC, AOP, Web, data access, transactions, caching, security, async and events, monitoring. But knowledge is scattered; real development demands you assemble them into one project that runs as a whole. Starting here, four articles deliver a complete, launch-ready project: BeeOrder, the Hive Order Center.

13 / 157

First, the boundaries. It is an e-commerce order-backend monolith that runs the main path from "a user registers" to "a user receives goods":

14 / 157
Table
ItemDefinition
NameBeeOrder, the Hive Order Center
Forme-commerce order-backend monolith
Six modulesuser / product / inventory / order / payment / notify
Core featuresregister and login, browse products, checkout, stock deduction, payment callback (simulated), order query, auto-close on timeout
Tech stackSpring Boot 3.2 + MySQL 8 + Redis 7 + MyBatis + Spring Security + JWT + Actuator + Docker Compose
Package rootcom.beeorder
15 / 157

Equally important is a not-to-do list — stating what you will not build prevents scope creep better than listing features:

16 / 157
  • No frontend: every deliverable is a REST API, verified with curl or Postman (#46 shows an optional Docker-hosted UI, but it is not required).
  • No real payments: real payment needs merchant credentials and keys; we use a "mock payment gateway" plus a callback endpoint to fully reproduce the state and idempotency of payment.
  • No microservice split: a monolith first, with clean boundaries; the end of this article discusses "when to split", but this project does not.
  • No big data or recommendations: operational stats stop at "daily order count and revenue".
17 / 157
Note

the clearer the scope, the less the next three articles drift. This statement is the "contract" for all subsequent code.

18 / 157
Section
2. Requirements in practice: unfold one sentence into user stories
19 / 157

The initial requirement is often just one sentence: "users can place orders." But how much code that means, and when it is done, nobody can say. Requirements analysis turns vague expectations into acceptance-testable user stories.

20 / 157

The standard form is "As a ⟨role⟩, I want ⟨capability⟩ so that ⟨value⟩", with acceptance criteria attached. BeeOrder's core story cards:

21 / 157
Table
RoleUser storyAcceptance criteria (testable)
Visitorregister and log in so I can orderphone is unique; password stored with BCrypt; login returns a JWT valid for 2 hours
Userbrowse the product list and details to choosepaginated; delisted products do not appear
Usersubmit an order to buy productssucceeds only if stock is sufficient; on success stock is deducted and status is CREATED
Userpay for an order to complete the purchaseon a successful callback the status becomes PAID; repeated callbacks do not double-charge
Userview my orders to track progressonly my own orders; filter by status
Systemauto-close unpaid orders to release stock30 minutes after creation with no payment, status becomes CLOSED and stock is restored
Operatorview order statistics to understand the businessdaily order count and revenue
22 / 157

Notice the last two rows — they are not triggered by "a user click" but are system behaviors. Such requirements are the most easily forgotten, yet they directly determine whether stock is released correctly.

23 / 157
Tip

acceptance criteria must be testable. "Checkout should be fast" is not a requirement; "P99 latency under 500ms" becomes a load-test case.

24 / 157

The table above is the finished product; the animation below plays it backwards — watch one vague wish get cornered by three questions into a testable story card:

25 / 157
Animation
Animation · How one sentence becomes a story card
Animation · How one sentence becomes a story card
26 / 157

The question most often skipped is "in which scene", and that is exactly where system behaviours like "close unpaid orders after 30 minutes" come from. A story whose acceptance criteria will not come out is a story whose questions have not been asked to the end.

27 / 157
Section
3. The domain model: entities, aggregates and business rules
28 / 157

With requirements clear, first find the "nouns" of the world. BeeOrder has seven core domain objects:

29 / 157
Table
EntityKey fieldsMeaning
Userid, phone, password, nicknamethe login subject
Productid, name, price, status, stock_snapshota sellable unit; price stored in cents
Inventoryproduct_id, available, lockedsellable and locked quantities
Orderid, user_id, total_amount, statusthe aggregate root carrying items
OrderItemid, order_id, product_id, price, quantitythe price snapshot at checkout
Paymentid, order_id, trade_no, amount, statusone payment attempt
InventoryLogid, product_id, change, type, ref_idan audit record of every stock change
30 / 157

The relationships: a user has many orders; an order contains many items; each item points to a product; each product has one inventory record; a payment maps to an order. The order is the aggregate root — changing items must go through the order.

31 / 157

Key business rules must be written down, because they map directly to code branches and constraints:

32 / 157
  • Order total = Σ(unit price × quantity), stored as DECIMAL or cents; never double.
  • Stock can never go negative: deduction must be guarded at the database level with a conditional update (WHERE available >= ?).
  • Status changes only follow the state machine; no skipping from CREATED straight to COMPLETED.
  • Only one valid payment per order: repeated callbacks must be idempotent.
  • Every change to money or stock leaves a ledger entry for reconciliation.
33 / 157
Key point

the domain model is not "a copy of the database tables". Think through the business rules and aggregate boundaries first; the schema is their consequence — do not reverse the order.

34 / 157
Section
4. The order state machine: the heart of the project (the key part)
35 / 157

Order status is the project's most central abstraction. Once the states are muddled or the transitions loose, you get dirty data like "a cancelled order receiving a successful payment". BeeOrder has six states:

36 / 157

CREATED → PAID → SHIPPED → COMPLETED, plus two terminal branches, CLOSED and REFUNDED. The full transition table:

37 / 157
Table
CurrentEventTargetSide effects
CREATEDpayment success callbackPAIDrecord payment ledger, convert locked stock to deducted, notify
CREATED30-minute timeout / user cancelCLOSEDrelease locked stock, notify
PAIDoperator shipsSHIPPEDrecord ship time, notify
SHIPPEDuser confirms receiptCOMPLETEDterminal, may trigger points (notify module)
PAIDuser refunds successfullyREFUNDEDrecord refund ledger, optionally restore stock
COMPLETED——terminal, no further transitions
CLOSED / REFUNDED——terminal, no further transitions
38 / 157

That table is the state machine's entire statute book, but reading statutes three times beats one glance at the picture. Drawn as a single map you can tape next to your monitor:

39 / 157
Diagram
Figure · The order state machine at a glance
Figure · The order state machine at a glance
40 / 157

Read it by the mnemonic "one main line, two terminals, three conditions": the main line CREATED → PAID → SHIPPED → COMPLETED only moves rightward; CLOSED and REFUNDED each enter through one designated door and never leave; and the three conditions (state matches event, target is allowed, side effects transactional) are written into validation code in the next article.

41 / 157

Every transition must satisfy three conditions: the current state matches the event, the target is in the allowed set, and side effects run in the same transaction or via reliable events. Turning this table into an enum plus a validation method in code is next article's job.

42 / 157
Trap

the worst state-machine habit is hard-coding states as strings. order.setStatus("paid") with a typo compiles fine and only fails in production when the status does not match. Use the enum OrderStatus.PAID so mistakes surface at compile time.

43 / 157
Section
5. Technology choices: every choice must explain "why not the alternative"
44 / 157

Choosing a stack is trade-offs, not name-dropping. BeeOrder's choices:

45 / 157
Table
AreaChoiceAlternativeWhy (vs the alternative)
FrameworkSpring Boot 3.2Spring MVC 5 / Quarkusrichest ecosystem and docs; 3.2 supports virtual threads natively, JDK 17+
DatabaseMySQL 8PostgreSQLhighest team familiarity and ops material; InnoDB row locks suit stock deduction
CacheRedis 7local Caffeineneed multi-instance shared hot cache and distributed locks, which local cache cannot do
PersistenceMyBatisSpring Data JPAstock deduction and reporting need precise SQL; MyBatis keeps SQL fully controllable
SecuritySpring Security + JWTSession + Redisstateless, fits multi-instance and decoupled frontends naturally
MonitoringActuator + Prometheusin-house instrumentationworks out of the box in a standard format; no wheel to reinvent
DeploymentDocker ComposeKubernetesCompose suffices for a monolith; Kubernetes complexity is a burden here
46 / 157

Each row deserves the question "why not the other one". For example, why MyBatis over JPA: the project's two most critical queries — UPDATE inventory SET available = available - ? WHERE product_id = ? AND available >= ? (atomic deduction) and the daily aggregation for reports — are exactly "precise SQL control" scenarios, where JPA's auto-generation gets in the way.

47 / 157
Decision
Decisionfor BeeOrder's persistence, do you choose MyBatis or Spring Data JPA?
48 / 157
Section
6. Layering and module boundaries
49 / 157

BeeOrder uses a layered monolith with four layers, top to bottom, and a one-way request flow:

50 / 157
Diagram
Figure 1 · BeeOrder architecture
Figure 1 · BeeOrder architecture
51 / 157
  • Access layer (Controller): protocol adaptation only — validation, DTO to domain conversion, calling a service, wrapping a uniform response. No business logic here.
  • Application layer (Service): business orchestration and transaction boundaries. Checkout is "validate stock -> create order -> deduct stock -> record ledger", and that orchestration belongs here.
  • Domain and data layer (Mapper / Repository): MyBatis handles persistence, Redis serves cache and distributed locks, MySQL is the single source of truth.
  • Infrastructure: Actuator provides observability, logging spans the whole path, and Docker Compose handles local and deployment orchestration.
52 / 157

Read the same picture once more, this time asking per layer, top to bottom: "did this layer reach over and grab someone else's job?" The four chips in the access layer are all protocol work (auth, validation, conversion, wrapping responses); the four in the application layer are about who orchestrates whom; the data layer only stores and fetches, it judges nothing. Finish the sweep and you will find not one line pointing back up:

53 / 157
Diagram
Figure · Architecture with dependencies flowing downward
Figure · Architecture with dependencies flowing downward
54 / 157

Module boundaries are expressed as packages. Within one module, split by business, and inside each package by controller / service / mapper / domain / dto:

55 / 157
text
com.beeorder├── user        # register, login, JWT issue and parsing├── product     # product list and detail (read-heavy, cached)├── inventory   # stock query, atomic deduction, inventory ledger├── order       # order aggregate, state machine, checkout/query/close├── payment     # payment creation, callback handling, idempotency├── notify      # SMS / in-app notifications (event-driven, async)└── common      # uniform response, exceptions, JWT util, constants, config
56 / 157

Boundary discipline: cross-module calls go one way — "upper calls lower", "order calls inventory". Two-way dependencies are forbidden. order depending on inventory and product is fine; inventory depending back on order creates a cycle and must be avoided.

57 / 157
类比|Analogy

picture this rule as the service passage of a restaurant — front of house (Controller) passes tickets back to the kitchen (Service), the kitchen pulls stock from the storehouse (Mapper), all one way. The day someone rules that "the storekeeper gets to instruct the waiter how to greet guests", both sides wait for the other to speak first and the shop never opens. That is exactly the circular-dependency error you get at startup: two beans waiting for each other.

58 / 157

See what that discipline governs, one click at a time — the first four nodes are each layer's "never", and the last is the bill for breaking it:

59 / 157
Diagram
FlowFour stations and the arrow that must not point back1 / 5
Click left to right: the first four nodes are each layer's never, the last is the price of breaking it
→
→
→
→
1. Controller · the counter
Protocol adaptation only: validation, DTO-to-domain conversion, wrapping a uniform response. Business orchestration must not appear on this desk — once it does, changing an endpoint drags the business along.
All clearFour nevers plus one downward-only arrow; the bill for breaking it always arrives after you press start.
60 / 157

After the last node, look back at the endpoint list in Section 7: every endpoint walks down this chain, none of them reaches the Mapper directly.

61 / 157

So which module actually deserves to leave the monolith? Do not guess — measure it on two axes: horizontal is how often the package changes, vertical is whether it needs to scale on its own.

62 / 157
Diagram
Figure · What should you split modules along?
Figure · What should you split modules along?
63 / 157

Two conclusions are enough to read this chart: common, top right, ripples through the entire project when touched, so tests freeze it in place; inventory, top left, is the only package that is both write-heavy and narrow-interface, so on the day a split really happens it is the first seam. A boundary is not a service — order and product, bottom right, change weekly yet need nothing more than a one-way dependency.

64 / 157
Note

when should you split into microservices? Only when a module needs independent scaling (say inventory writes far outweigh the rest), an independent release cadence, or its own database. A monolith suffices here; and because the packages draw clean boundaries, extracting inventory later touches a controlled surface — exactly the value of "monolith first, clear boundaries".

65 / 157
Section
7. API overview
66 / 157

The API contract is the "contract" for frontend/backend collaboration. BeeOrder uses the prefix /api/v1. The full list (about 15 endpoints):

67 / 157
Table
MethodPathDescriptionAuth
POST/api/v1/auth/registerregisterpublic
POST/api/v1/auth/loginlogin, returns a JWTpublic
GET/api/v1/auth/mecurrent user infologged in
GET/api/v1/productsproduct list (paginated)public
GET/api/v1/products/{id}product detailpublic
GET/api/v1/inventory/{productId}query stock for a productpublic
POST/api/v1/ordersplace an orderlogged in
GET/api/v1/ordersmy orders (filter by status)logged in
GET/api/v1/orders/{id}order detaillogged in
POST/api/v1/orders/{id}/cancelcancel an orderlogged in
POST/api/v1/paymentsstart payment for an orderlogged in
POST/api/v1/payments/callbackpayment gateway async callbackpublic (signature verified)
GET/api/v1/payments/{orderId}query the payment record of an orderlogged in
GET/api/v1/notificationsmy notificationslogged in
GET/api/v1/admin/stats/ordersoperator order stats (daily)admin
68 / 157

Conventions: all responses are wrapped as { "code": 0, "message": "ok", "data": ... }; code != 0 signals a business error. /payments/callback skips JWT but must verify the gateway signature and be idempotent — it is the only publicly exposed write endpoint and the largest attack surface.

69 / 157
Tip

once an API is on this list, the third article may only add to it (changes must sync the contract). So spend ten extra minutes now to pin down paths, verbs and auth.

70 / 157
Section
8. Project layout: single module or multi-module?
71 / 157

One last structural decision before coding: multi-module Maven, or a single module with packages? Many teams immediately split into beeorder-user, beeorder-order and so on, then find that touching one API means editing three poms and every build is slow.

72 / 157
Table
OptionProsConsFits
Multi-modulestrong compile boundaries, independently packageablecomplex poms, painful cross-module refactors, slower startuplarge teams with genuine independent releases
Single modulesimple, IDE-friendly, fast refactorsboundaries enforced by discipline, not the compilersmall/medium projects, monolith phase
73 / 157

BeeOrder picks the single-module layout. The tree:

74 / 157
Code
Codetext
beeorder├── pom.xml├── docker-compose.yml├── src/main/java/com/beeorder│   ├── BeeOrderApplication.java│   ├── common│   │   ├── response/       # Result, ErrorCode, global exception handling│   │   ├── security/       # JwtUtil, JwtFilter, SecurityConfig│   │   └── config/         # MyBatis, Redis, Async config│   ├── user/               # controller / service / mapper / domain / dto│   ├── product/│   ├── inventory/│   ├── order/│   ├── payment/│   └── notify/├── src/main/resources│   ├── application.yml│   └── mapper/             # MyBatis XML (complex SQL lives here)└── src/test/java/com/beeorder
Notes
  • Every business package keeps the same controller / service / mapper / domain / dto layering, consistently across the project
  • Complex SQL goes in resources/mapper/*.xml, simple queries use annotations — readable and controllable
  • The test tree mirrors the main tree: unit tests for the order package go in test/.../order
75 / 157

Section 8's conclusions plus the quadrant from Section 6 make one good matching game: the left is each module's or option's situation, the right is the disposition this article prescribes. A wrong pair explains itself on the spot:

76 / 157
Match
MatchHow to dispose of each module: situation meets decisionMatched 0/6 · Missed 0
Left is the situation of each module or option; right is the decision from Sections 6 and 8. A wrong pair explains itself immediately.
Pick a card on the left first
77 / 157
Key point

a single module does not mean "no boundaries". We partition modules by package and require one-way cross-module dependencies, keeping complexity within a range that does not hurt development speed.

78 / 157
Section
9. Iteration plan: mapping the four deliverables
79 / 157

The project splits into four iterations, matching four articles, each with clear deliverables:

80 / 157
Table
IterationArticleDeliverable
1 Designthis one (#43)user stories, domain model, state machine, API list, layout
2 Modeling and API#44table DDL, entities and mappers, controller and DTO contract
3 Core business#45checkout, atomic stock deduction, idempotent callback, auto-close (TX and cache)
4 Delivery#46unit/integration tests, CI/CD, Docker Compose deployment and Actuator observability
81 / 157
Animation
Animation · From design to production
Animation · From design to production
82 / 157

Use that animation as a progress bar: its six frames land on the four rows of the table above — frame 1 is this article, frame 2 is #44, frame 3 is #45, frames 4-6 all belong to #46. When a frame stalls you, look up which row you are standing in. Each frame delivers exactly one thing, and nothing moves on until the previous one is accepted — that discipline keeps four articles from ending as four piles of code that never run together:

83 / 157
Animation
Animation · The delivery plan and where you are now
Animation · The delivery plan and where you are now
84 / 157
Note

the dependency between iterations is one-way — the implementation in (3) strictly follows the API list and state machine fixed in (1). That is why this article must nail the definitions: it is the shared source of truth for the next three.

85 / 157
Section
10. Risk list: write down the traps early
86 / 157

Listing risks at kickoff beats a post-mortem after an incident. BeeOrder's four main risks:

87 / 157
Table
RiskImpactResponse
Overselling stockselling goods that do not exist, financial lossconditional update WHERE available >= ? plus a transaction; separate pre-lock and deduct
Duplicate paymentcharging the same order twiceidempotent callback (unique key trade_no) plus state-machine validation
Lost messagesmissing notifications/points, user complaintslocal message table + scheduled compensation + idempotent consumer (see #41)
Slow large-table queriesorder lists get slower over timecomposite index on user_id + status; cursor pagination; archive old orders if needed
88 / 157
类比|Analogy

this risk list is a pilot's pre-flight checklist. The captain does not doubt his skill — he refuses to trust his memory, so every item must be confirmed by one concrete action (writing down the mitigation) instead of "I'll remember it". The second row, duplicate payment, works the same way: the same cinema ticket must never be printed twice, and what guarantees it is not "let me first check whether it was already printed" but a ticketing system that honours only the first request for that ticket number. In code that discipline is called an idempotency key, and #44 Section 8 turns it into a unique index.

89 / 157
Kernel lab
90 / 157

Before freezing the design, run four labs that show what this architecture actually prevents. Lab 1: layers and dependency direction — it draws that restaurant service passage as real calls:

91 / 157
Kernel lab
TeaVMOne checkout travelling top-down through four layersidle
Start on 'an order, top-down' and watch the arrow only move downward; then switch to 'cost of skipping a layer' to see how many places the same validation has to be rewritten once the Controller calls the Mapper directly
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
92 / 157

Lab 2: one request across the whole site. The architecture picture shows four boxes, but a request walks a far longer road — authentication, validation, transaction, stock deduction and messaging each live at a different stop:

93 / 157
Kernel lab
TeaVMThe full journey of one checkout requestidle
Run 'happy path' first and count the stops; then switch to 'validation fails' and 'business error' to see where the request dies and who translates it into an error code a user can read
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
94 / 157

Lab 3: conditional assembly, which answers "same feature, but staging and production must behave differently". Conditional annotations — the @Conditional family, the gate that decides whether a bean gets wired based on whether a class exists, a bean exists or a property is set — are the bouncers of auto-configuration:

95 / 157
Kernel lab
TeaVMHow the mock payment gateway gets wired on demandidle
Pick '@ConditionalOnBean' to see how bean ordering decides success or failure, then 'evaluation report' to read a real auto-configuration verdict list
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
96 / 157

Lab 4: your own starter, which packs all three lessons into one box. A starter — a bundle of dependencies plus auto-configuration, ready to use the moment you declare it — is literally "other people's hard-won lessons" put in a box. BeeOrder will pull in spring-boot-starter-web, mybatis-spring-boot-starter and friends, so it is worth knowing what is inside a box:

97 / 157
Kernel lab
TeaVMAssembling a custom starter end to endidle
Walk 'auto-config entry -> properties binding -> bean creation -> turning it off' and watch beeorder-pay-starter go from a pom line to a live bean, then kill the whole thing with one property
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
98 / 157

Lab 5: type those conclusions into a real container. The four labs above let you look; this console lets you type — boot the container, name the beans, then run layering, layer-skipping, the request journey and condition evaluation as one pipeline:

99 / 157
Console
100 / 157

Once beans has listed them, recall the layer picture from Section 6: every bean's seat is decided by its package and the direction of its dependencies — far more intuitive than memorising a directory tree.

101 / 157
Decision
Decisionshould BeeOrder split into microservices from the start, or be a monolith first?
102 / 157
Summary

this article wrote not one line of business code, yet it determines the fate of the next three. The essence is three things: unfold "users can place orders" into user stories with acceptance criteria; nail down the order state machine and domain rules in writing; and explain the trade-offs behind module boundaries and technology choices. BeeOrder's boundaries, state machine, API list and layout are frozen. Next article we build the tables, write the mappers and fix the controller contract according to this contract. With the foundation solid, building upward is fast.

103 / 157
Section
11. Sandbox: three ways to slice the same project
104 / 157

Architecture will not change how long your day is, but it decides how many files one requirement has to touch. The sandbox below folds Sections 6 and 8 into a single three-position switch; each position shows side by side whether the app boots, how many files an added column touches, and whether cross-module calls now go over the network:

105 / 157
Sandbox
SandboxSingle module / multi-module Maven / microservices
Result
Add a recipient field: 4 files (entity / DTO / VO / one ALTER TABLE)
Full mvn test: 38s
Startup: 2.4s
# package boundaries hold only by discipline — one wrong IDEA import reaches a sibling package
Risk: someone lets inventory import order the other way, and the compiler says nothing
Monolith with packages: fastest to develop, but every boundary lives in your head. That is why the circular-dependency row in Section 13 must ring a bell.
106 / 157
Tip

of the three positions only the last is irreversible — a monolith can be split, but almost nobody ever merges microservices back. So the order never changes: monolith first, boundaries drawn honestly, split along those seams only when they truly burst.

107 / 157
Section
12. Check yourself
108 / 157

A warm-up question on how to read the technology-choice table in Section 5:

109 / 157
Quiz
Check yourselfDiscussing persistence, a teammate says 'let's use JPA, it saves us writing SQL'. Measured against this article's standard, what is wrong with that reasoning?
Pick one — you get feedback right away
110 / 157

Now a combined question threading Sections 6, 7 and 10:

111 / 157
Quiz
Check yourselfTo get the auto-close job working early, a colleague adds 'import com.beeorder.order.OrderService' inside the inventory package; order already depends on inventory. What is the most likely outcome?
Pick one — you get feedback right away
112 / 157
Section
13. Common errors, searchable by exact wording
113 / 157

However tidy the design doc, day one of coding still hits walls. Copy each fragment below straight into a search box — do not paraphrase or shorten it.

114 / 157
Table
Error text (fragment)What really happened30-second fixRead more in
Field orderMapper in com.beeorder.order.service.OrderService required a bean of type 'com.beeorder.order.mapper.OrderMapper' that could not be foundThe package sits in the wrong place: @SpringBootApplication scans its own package and its sub-packages only. Put the mapper under com.beeorder.dao (a sibling of com.beeorder.order) or outside the startup class entirely and the container simply never sees itFirst check that the startup class is the common ancestor of every business package; if the class really lives outside, name it explicitly with @MapperScan("com.beeorder.**.mapper") instead of blasting a wide @ComponentScanSection 6 · #16 Hello Spring Boot
APPLICATION FAILED TO START + The dependencies of some of the beans in the application context form a cycle + orderService ==> inventoryService ==> orderServiceA reverse cross-layer or cross-module call closed a loop. The usual trigger is the quiz above: low-level inventory importing high-level order. Note that since Spring Boot 2.6 spring.main.allow-circular-references defaults to false, so both constructor and field injection stop here; flip it to true and only the field-injection side gets talked through by the three-level cacheBreak one edge of the cycle the log draws: pull the shared logic into common or a new package so arrows run one way again. Do not treat that switch as the cure — it merely hides the architecture debt until runtimeSection 6 · #10 Circular dependency
org.springframework.dao.DuplicateKeyException: Duplicate entry '10015-9f2c1e0a' for key 'order.uk_order_idem'A database unique index shut out a duplicate submit — this is not a bug, it is the anti-double-order defence from the risk list in Section 10 genuinely firingDo not let it reach the user: catch it in the service and return the first result unchanged (that is idempotency) rather than throwing a 500. Code in #44 Section 8#44 Idempotency · #31 Transaction internals
Cancelling a paid order returns code=0; the refund happens but the status stays PAID@Transactional sits at the wrong layer: it annotates a private service method, or is reached through this.xxx() self-invocation, or you moved "refund + update status" straight into the Controller calling the mapper twice. Transactions ride the proxy, and none of these give the proxy a chance, so each update commits on its ownMove @Transactional onto the public orchestration method of the application-layer service and make sure it is called from outside (the "where the tx boundary sits" position of Lab 1 in Section 10 is exactly this scene)#31 Transaction internals · #12 Dynamic proxy
WARN ... : Cannot enhance @Configuration bean definition 'xxxConfig' since its singleton instance has been created too early, or a @Bean unexpectedly missingAuto-configuration and your own config race: condition evaluation happens during bean-definition registration, so @ConditionalOnBean matched against an ordinary @Component frequently reads "not there yet" at the moment of the decisionUse @ConditionalOnProperty or @ConditionalOnMissingBean for ordinary beans and reserve @ConditionalOnBean for auto-configuration classes. Run with --debug and read the condition evaluation report for the verdict#19 Conditional beans · #18 Auto-configuration
No qualifying bean of type 'com.beeorder.common.config.RedisProperties' available (the custom starter was imported but did nothing)The starter trio: ① the auto-configuration class was never registered in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports; ② the properties class lacks @EnableConfigurationProperties or setters; ③ the prefix in application.yml is misspelled, so binding silently matches nothingWalk the four steps "entry -> properties binding -> bean creation -> turning it off" one by one; Lab 4 in Section 10 turns them into switches#20 Your own starter · #21 Config and profiles
Error starting ApplicationContext. ... Consider defining a bean of type 'org.springframework.jdbc.core.JdbcTemplate' in your configurationOnly the mysql-connector-j driver was added without a datasource starter, so the auto-configuration condition "class present" fails and not even a pool is builtAdd spring-boot-starter-jdbc (or the MyBatis starter), then check the wiring report via logging.level.org.springframework=DEBUG to see it being skipped#27 JDBC evolution · #18 Auto-configuration
115 / 157
Tip

five of those seven share one root — the container never got hold of that thing. When beginners read required a bean ... could not be found the reflex is to add an annotation; the right reflex is three questions: is it under the startup class's package tree? Is its starter on the classpath? Does the auto-configuration condition actually hold?

116 / 157

That cycle in the table's second row reads very differently as the original text. Here is the raw report; every fragment is clickable, and a wrong pick still tells you why it is not the one:

117 / 157
Triage
Error triageAPPLICATION FAILED TO START: form a cycle
The report behind a backward cross-module call

If you actually did the quiz from Section 12 in code, the tail of the startup log looks like this. First time round it is tempting to stare at the last sentence, while the real information sits higher up.

***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| inventoryService (field com.beeorder.order.service.OrderService com.beeorder.inventory.service.InventoryService.orderService)
↑ ↓
| orderService (field com.beeorder.inventory.service.InventoryService com.beeorder.order.service.OrderService.inventoryService)
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default.
To allow circular references, set spring.main.allow-circular-references to true.
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
118 / 157

One line of review is enough: the only spot you actually edit is that backward import inside the inventory package; every other frame explains why it is not the one.

119 / 157
Section
14. Practice in three levels
120 / 157

Three tiers: level 1 runs if you type it in; finish level 3 and you own your own architecture review checklist.

121 / 157
Section
Level 1 · Follow along
122 / 157

Goal: assemble the BeeOrder skeleton yourself, then deliberately break the wiring once and watch the could not be found failure happen.

123 / 157

Step one, a new pom.xml (Spring Boot 3.2.5 + Web + JDBC; it starts fine without MySQL):

124 / 157
xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>3.2.5</version>        <relativePath/>    </parent>    <groupId>com.beeorder</groupId>    <artifactId>beeorder</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>beeorder</name>    <properties>        <java.version>17</java.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-validation</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-jdbc</artifactId>        </dependency>    </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>
125 / 157

No need to type that pom by hand — tick it out. The three defaults (Web / Validation / JDBC) are exactly the minimal skeleton level 1 needs; then look over the dependencies the next three articles will pull in — know the total first, then decide what actually goes in today:

126 / 157
Generator
GeneratorBeeOrder's pom: tick out the minimal runnable skeletonpom.xml3 / 10
The three defaults are the minimal combination level 1 needs; then tick MyBatis, MySQL, Redis, Security and Actuator one by one and watch the line count grow. Every extra starter runs another batch of auto-configuration at startup and widens the surface for errors — so do not import everything at once.
Output
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
Why each choice matters
parentInheriting 3.3.4 starter-parent means no spring-boot-starter-* needs a version; the moment someone adds an explicit version to one starter, that one wins — the most common source of dependency drift.
WebAnything that serves HTTP needs it: DispatcherServlet, embedded Tomcat and JSON mapping come inside this starter.
ValidationWithout it @Valid silently does nothing — @NotNull on its own checks no one.
JDBCJust the template class and HikariCP — the minimum when you refuse an ORM.
127 / 157

Step two, create the packages from Section 8 and write the startup class plus two minimal classes. The startup class must sit in com.beeorder, the common ancestor of every business package:

128 / 157
java
package com.beeorder;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class BeeOrderApplication {    public static void main(String[] args) {        SpringApplication.run(BeeOrderApplication.class, args);    }}
129 / 157
java
package com.beeorder.order.mapper;import org.springframework.stereotype.Repository;@Repositorypublic class OrderMapper {    public String ping() {        return "mapper-ok";    }}
130 / 157
java
package com.beeorder.order.controller;import com.beeorder.order.mapper.OrderMapper;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/api/v1/ping")public class PingController {    private final OrderMapper orderMapper;    public PingController(OrderMapper orderMapper) {   // constructor injection: complete at birth        this.orderMapper = orderMapper;    }    @GetMapping    public String ping() {        return orderMapper.ping();    }}
131 / 157

Step three, start it and call it:

132 / 157
bash
mvn spring-boot:runcurl http://localhost:8080/api/v1/ping
133 / 157

Expected response — one bare line, no JSON envelope (the envelope is next article's job):

134 / 157
text
mapper-ok
135 / 157

Step four, break the wiring. Rewrite OrderMapper.java the way MyBatis projects really write it — an interface plus @Mapper — and move it from com.beeorder.order.mapper to com.beeorder.dao. Notice it is still under the startup class's subtree, so "outside the scanned package" is not the excuse this time:

136 / 157
java
package com.beeorder.dao;import org.apache.ibatis.annotations.Mapper;@Mapperpublic interface OrderMapper {    String ping();}
137 / 157

Restart, and the console stops right here:

138 / 157
text
***************************APPLICATION FAILED TO START***************************Description:Field orderMapper in com.beeorder.order.controller.PingController required a bean of type 'com.beeorder.dao.OrderMapper' that could not be found.Action:Consider defining a bean of type 'com.beeorder.dao.OrderMapper' in your configuration.
139 / 157

Two layers of cause, the second more subtle: ① an interface has no implementation for the container to instantiate reflectively — a mapper instance is produced on demand by MyBatis' dynamic proxy, never by component scanning; ② @Mapper carries no @Component semantics, so being inside the scanned subtree changes nothing. Without mybatis-spring-boot-starter (or without telling it where to find the interfaces), the chain snaps at the wiring stage.

140 / 157

Step five, repair it: add @MapperScan("com.beeorder.dao") to the startup class and make sure the MyBatis starter is in pom.xml. Restart, curl again, and mapper-ok returns. The lesson you keep forever: "can the package be scanned" and "who wires this kind of object" are two different questions — ordinary classes via component scan, mapper interfaces via @MapperScan, third-party pieces via starter auto-configuration.

141 / 157

Acceptance checklist: ① explain why step four failed and step five succeeded; ② name the single word in that error that actually decides the outcome (scan); ③ switch the controller from constructor injection to a field @Autowired and restart — it still fails, because "does this bean exist" is answered during wiring regardless of injection style. Say that clearly and you have separated "missing bean" from "bean ordering".

142 / 157
Section
Level 2 · Variants
143 / 157

Change exactly one thing per run and the conclusion flips:

144 / 157
  1. Build a reverse dependency across layers: give OrderMapper a private final OrderService orderService; (constructor injection) while OrderService constructor-injects OrderMapper. You will observe startup failing with the log drawing orderService ==> orderMapper ==> orderService. Now switch the OrderMapper side to field injection: on Boot 3 it still fails, because spring.main.allow-circular-references has defaulted to false since Spring Boot 2.6; only when you set it to true does the field-injection side get through via the three-level cache — the app runs, but the module boundary has collapsed.
  2. Move @Transactional off OrderService's public orchestration method onto a private helper in the same class and call it through this.helper(). You will observe neither variant raising anything nor rolling back — transactions ride the proxy, and the proxy can only intercept "a public method called in from outside". Put the annotation back on the public method, invoked by the controller through the injected bean, and the second failing write now undoes the first. Compare with the "where the tx boundary sits" position of Lab 1 in Section 10; both should tell the same story. (Do not try to prove it with getClass(): only beans that actually match advice get proxied, and controllers usually do not.)
  3. Delete spring-boot-starter-jdbc from pom.xml. You will observe the app booting normally as long as nothing injects JdbcTemplate — dropping a starter drops capability, it need not error; the moment something injects it, startup reports No qualifying bean. Cross-check the condition evaluation report from --debug and find the DataSourceAutoConfiguration line marked matched: false.
145 / 157

Tip: after variant 1, reopen the second row of the table in Section 13 — both should match exactly.

146 / 157
Section
Level 3 · Build one
147 / 157

Write yourself an architecture health check you can run on any future project before touching its code.

148 / 157

Requirements:

149 / 157
  • A LayerRulesTest (plain JUnit + reflection, no business class may be modified) that walks every .java file under src/main/java reading its package and import statements
  • Enforce three rules: ① no controller package imports a mapper package; ② no package imports a class from order that in turn imports back (detecting cycles of length 2); ③ common imports no business package
  • On violation print a table: file, line, offending import, which rule broke, suggested extraction
  • Ship an ARCHITECTURE.md template stapling the Section 6 layer diagram, the Section 7 endpoint list and this rule table together
150 / 157

Acceptance checklist: ① run it on the current skeleton and rule ① immediately catches PingController importing OrderMapper; ② insert an OrderService as suggested and the test goes green; ③ fabricate an inventory -> order -> inventory cycle and the script reports it in the test phase instead of waiting for the app to fail at boot; ④ the whole exercise changes zero lines of business code.

151 / 157
Section
15. Self-check
152 / 157
Self-check

without looking back, order BeeOrder's four layers from nearest to furthest from the user, and name the one thing each layer is never allowed to do.

153 / 157
Self-check

what is missing from "users can place orders" that disqualifies it as a requirement? Rewrite it as a story card whose acceptance criteria include at least two lines you could turn into tests directly.

154 / 157
Self-check

order may call inventory but not the reverse. Explain it with the restaurant analogy, then say at which stage a breach bites first — compile, startup or runtime.

155 / 157
Self-check

why must persistence be chosen from "the two hardest SQL statements in this project" rather than "which framework feels easier"? Recite both statements.

156 / 157
Self-check

which signals would justify extracting inventory from the monolith, and what must already be true before you attempt it?

157 / 157

Mantra: **requirements must be testable, status must follow the machine, arrows point only downward, boundaries live in packages, choose by the hardest job, split only when it truly bursts.**