A Frontend Developer's Guide to Backend Troubleshooting: curl, traceId, Logs, MySQL, and Redis
16 | (Frontend to Full-Stack) How Frontend Developers Troubleshoot Backend Issues: Using curl, traceId, Logs, MySQL, and Redis
GitHub Repository: https://github.com/2530622506/fullstack-mall
04 | (Frontend to Full-Stack) Why Is Frontend State Not Enough? From Page Data to MySQL Persistence
This chapter is a periodic wrap-up of this series. You have already read about Java, Spring Boot, Controller, MySQL, MyBatis-Plus, JWT, Redis, Products, SKU, Shopping Cart, Orders, Payments, and the subtitle practical exercise. Now it's time to establish a set of backend troubleshooting methodologies. The frontend code in this article is all "frontend-side illustrative code," used to draw analogies to browser DevTools and API debugging habits. This chapter is based on the current project's
ApiResponse,ApiCode,GlobalExceptionHandler,TraceIdFilter,SecurityConfig,ProductDetailCacheService, test classes, and README commands.
1. What Problem Does This Solve
When frontend developers troubleshoot problems, the common path is: open browser DevTools, look at Network, Console, Application, Elements; confirm the request URL, HTTP status, response JSON, localStorage token, page state, and component props. After transitioning to the backend, these habits are still useful, but not sufficient. Because backend problems can occur in Security Filters, Controller parameter binding, Bean Validation, Facade business rules, transactions, Mapper SQL, MySQL constraints, Redis caching, scheduled tasks, external callbacks, and test environments.
This chapter aims to help you establish a backend troubleshooting closed loop:
Phenomenon → HTTP Status Code → ApiResponse.code → traceId → Logs → Controller → Facade → DbService/Mapper → MySQL → Redis → Test Reproduction
You will learn:
- How to read the unified response
ApiResponse; - How to distinguish between 400, 401, 403, 404, 409, and 500;
- Why
traceIdis key for frontend-backend collaborative troubleshooting; - Why curl is more suitable than a browser for isolating backend problems;
- How to determine if a problem occurs in Security, Controller, Facade, the database, or Redis;
- How to use MySQL and Redis CLI to verify the source of truth;
- How to understand business rules from test names;
- How to avoid common backend pitfalls.
The most important shift for frontend developers is: don't just ask "Why is the page wrong?" Instead, ask "At which layer of the backend chain does the result already diverge from expectations?" Once you can pinpoint the layer, the problem is usually not difficult to fix.
2. Drawing Analogies with Frontend Knowledge
The Network panel in frontend DevTools tells you: request URL, method, request headers, request body, status code, response body. Backend troubleshooting also starts from this information but continues deeper into the service internals.
| Frontend Troubleshooting Action | Backend Corresponding Action | Example in This Project |
|---|---|---|
| Check Network URL | Check Controller path mapping | /api/products/{id}, /api/admin/products/{id}/subtitle |
| Check HTTP status | Check Security / ExceptionHandler | 401, 403, 404, 409, 500 |
| Check response JSON | Check ApiResponse.code |
PRODUCT_NOT_FOUND, VALIDATION_ERROR |
| Check Request Headers | Check JWT, Idempotency-Key, X-Mock-Payment-Secret |
Order idempotency, payment callback |
| Check localStorage token | Check JwtAuthenticationFilter parsing result |
Current user and role |
| Check page state | Check database fields and cache values | mall_order.status_code, Redis key |
| Console log | Server-side logs and traceId | product_detail_cache event=HIT |
| Mock API reproduction | JUnit / MockMvc / Testcontainers | PaymentMySqlContainerTest |
Frontend-side illustrative code:
// Frontend-side illustrative code: When an API error occurs, don't just display the message; log the code and traceId.
try {
await request.patch(`/api/admin/products/${id}/subtitle`, { subtitle })
} catch (error: any) {
const body = error.response?.data
console.error('Backend Error', {
httpStatus: error.response?.status,
code: body?.code,
traceId: body?.traceId,
data: body?.data,
})
}
The backend's traceId is like a troubleshooting number you assign to a single request. The frontend provides it to the backend, and the backend can then find the exception stack or business logs for that same request in the logs.
3. First, Understand the Unified Response
All business APIs in this project use ApiResponse<T> as the response envelope:
private String code;
private String message;
private T data;
private String traceId;
private Instant timestamp;
Frontend logic should primarily depend on code, not parse message. message is a human-readable prompt in Chinese, and its wording may change in the future; code is a stable business code. For example:
SUCCESS: Success;VALIDATION_ERROR: Request parameter validation failed;MALFORMED_JSON: JSON syntax error or enum conversion failure;UNAUTHORIZED: Not logged in or invalid Token;FORBIDDEN: Logged in but insufficient permissions;PRODUCT_NOT_FOUND: Product does not exist or is not publicly visible;ORDER_STATUS_CONFLICT: The current order status does not allow this operation;PAYMENT_CALLBACK_SECRET_INVALID: Mock payment callback secret is incorrect;INTERNAL_ERROR: Unhandled server-side exception.
flowchart TD
A[API Response] --> B[HTTP status]
A --> C[ApiResponse.code]
A --> D[traceId]
B --> E[Determine Category]
C --> F[Determine Business Reason]
D --> G[Correlate Server Logs]
Many frontend developers only look at the HTTP status and ignore the business code. For instance, a 404 might mean the URL doesn't exist, or it might mean a product is not listed and is treated as non-existent by the public API; a 409 might be an inventory version conflict, or it might mean the order status doesn't allow cancellation; a 400 might be a field validation failure, or it might be a JSON format error. Business codes can distinguish these situations.
4. HTTP Status Code Troubleshooting Table
4.1 400: Request Format or Field Validation Issues
400 commonly comes from two sources. The first is @Valid validation failure, such as an empty subtitle, a registration username that is too short, or an incorrect order idempotency key format. GlobalExceptionHandler.handleValidation returns VALIDATION_ERROR and lists the field names and prompts in data.fieldErrors.
The second is JSON parsing failure, such as a request body that is not valid JSON, an incorrect enum value, or a string passed for a numeric field. handleMalformedJson returns MALFORMED_JSON. If you find that the Controller breakpoint was not hit but a 400 is returned, the problem most likely occurred during the parameter binding phase.
4.2 401: Not Logged In or Invalid Token
401 occurs before the Controller and is returned by RestAuthenticationEntryPoint. Common causes: missing Authorization header, not in Bearer xxx format, expired Token, incorrect Token signature. When troubleshooting, frontend developers should first check the Request Headers in the Network panel.
curl -i http://localhost:8080/api/auth/me
This is expected to return 401. Only after adding the correct token will it return the current user.
4.3 403: Logged In but Insufficient Permissions
403 means the backend recognizes you, but you lack permissions. A regular user accessing /api/admin/** will be returned FORBIDDEN by RestAccessDeniedHandler. This differs from frontend route guards: even if the page doesn't display a button, a user directly curling the admin API must be blocked by the backend.
flowchart TD
A[Request Enters Security] --> B{Is Token Valid?}
B -->|No| C[401 UNAUTHORIZED]
B -->|Yes| D{Does URL Require ADMIN?}
D -->|No| E[Continue to Controller]
D -->|Yes| F{Is User ADMIN?}
F -->|No| G[403 FORBIDDEN]
F -->|Yes| E
4.4 404: URL Does Not Exist or Business Object Does Not Exist
If the URL is misspelled, such as /api/product/1 missing an s, it will be uniformly converted to NOT_FOUND by NoResourceFoundException. If a product does not exist, it might return PRODUCT_NOT_FOUND. Both have an HTTP status of 404, but the business codes differ. When troubleshooting, check the code first.
There is a special point about public product details: unlisted products will also appear as PRODUCT_NOT_FOUND to anonymous users, to avoid leaking drafts or delisted products. Do not assume that just because an ID exists in the database, the public API must return it.
4.5 409: Business Status Conflict
409 usually indicates that the request format is correct and the user has permissions, but the current business status does not allow the operation. For example, an illegal product status transition, an inventory version conflict, a paid order that cannot be canceled, or a closed payment transaction that cannot have a successful callback. When the frontend sees a 409, it should not blindly retry but should refresh the status or prompt the user.
4.6 500: Unexpected Server-Side Exception
500 is converted to INTERNAL_ERROR by GlobalExceptionHandler.handleUnknownException. The detailed stack trace is only written to server logs and is not exposed externally. When troubleshooting a 500, the most valuable information from the frontend is not a screenshot, but the traceId, request URL, request body, and time of occurrence.
5. How to Use traceId
TraceIdFilter generates or passes through X-Trace-Id for each HTTP request. If the frontend request header carries a valid traceId, the backend will continue to use it; if not, or if the format is invalid, the backend generates a new one and writes it back in the response header and body.
sequenceDiagram
participant FE as Frontend or curl
participant Filter as TraceIdFilter
participant API as Backend Business
participant Log as Server Logs
FE->>Filter: Request with optional X-Trace-Id
Filter->>Filter: Validate or generate traceId
Filter->>API: Request attribute with traceId
API->>Log: Log records traceId
API-->>FE: Response body and header return traceId
Manually specifying a traceId:
curl -i http://localhost:8080/api/products/1 \
-H 'X-Trace-Id: debugProduct001'
If the response also contains debugProduct001, it means the chain ID was passed through successfully. If an exception appears in the backend logs, you can search for this traceId.
6. curl Is the Primary Tool for Backend Debugging
Browsers are convenient, but they mix in factors like CORS, caching, page state, interceptors, routing, and component rendering. curl is more suitable for isolating the backend: you explicitly write the URL, method, headers, and body, and see the raw HTTP status and response.
Common templates:
# GET, display status code and response headers
curl -i 'http://localhost:8080/api/products/1'
# POST JSON
curl -i -X POST 'http://localhost:8080/api/auth/login' \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"Admin123456"}'
# With JWT
curl -i 'http://localhost:8080/api/auth/me' \
-H "Authorization: Bearer $ADMIN_TOKEN"
# With idempotency key
curl -i -X POST 'http://localhost:8080/api/orders' \
-H "Authorization: Bearer $USER_TOKEN" \
-H 'Idempotency-Key: debug-order-001'
# Simulate payment callback secret
curl -i -X POST 'http://localhost:8080/api/mock-payments/callback' \
-H 'X-Mock-Payment-Secret: local-mock-payment-secret' \
-H 'Content-Type: application/json' \
-d '{"paymentNo":"PAY-xxx","callbackId":"cb-001","providerTransactionNo":"mock-tx-001","result":"SUCCESS"}'
If curl works normally but the page is abnormal, the problem is likely in the frontend request construction, token storage, interceptors, state updates, or rendering. If curl is also abnormal, continue troubleshooting along the backend chain.
7. Locating Issues Layer by Layer from Controller to Database
Do not skip layers when troubleshooting the backend. Recommended order:
flowchart TD
A[API Phenomenon] --> B[Confirm Reproducible with curl]
B --> C[Check HTTP status and code]
C --> D{Did Controller Enter?}
D -->|No| E[Check URL, Security, Parameter Binding]
D -->|Yes| F[Check Facade Business Branch]
F --> G[Check DbService and Mapper]
G --> H[Check Final Data in MySQL]
H --> I[Check if Redis Has Old Value]
I --> J[Add Test to Lock Down Issue]
7.1 Controller Not Entered
Could be a wrong URL, wrong HTTP method, Security interception, JSON format error, or @Valid validation failure. Check the status code: 401/403 is mostly Security; 400 is mostly parameter binding or validation; 404 is mostly URL or business object.
7.2 Controller Entered but Facade Result Incorrect
Check if the Facade received the correct current user, trimmed the input, and entered the correct business branch. For example, in product details getPublishedProduct, a NULL_HIT directly restores a 404, while a MISS queries the database.
7.3 DbService Affected Rows Incorrect
For update-type interfaces, check the UPDATE affected rows. Inventory, orders, and payments rely more on affected rows to determine the winner in concurrency. An affected row count of 0 does not necessarily mean SQL failure; it could be that the old status condition was not met.
7.4 MySQL Correct but API Incorrect
Check the Response assembly and Redis cache. The subtitle modification is a typical example: MySQL already has the new value, but the public detail is still old, possibly because the old cache was not deleted, or ProductDetailResponse / toDetailResponse did not include the new field.
8. Manual Checks for MySQL and Redis
8.1 MySQL Is the Source of Truth
Products:
docker exec fullstack-mall-mysql mysql -umall -pmall123 fullstack_mall \
-e "SELECT id, title, subtitle, status_code, updated_at FROM mall_product WHERE id = 1;"
Inventory:
docker exec fullstack-mall-mysql mysql -umall -pmall123 fullstack_mall \
-e "SELECT id, product_id, available_stock, locked_stock, version FROM mall_product_sku WHERE id = 1;"
Orders and Payments:
docker exec fullstack-mall-mysql mysql -umall -pmall123 fullstack_mall \
-e "SELECT id, order_no, status_code, total_amount, expires_at FROM mall_order ORDER BY id DESC LIMIT 5;"
docker exec fullstack-mall-mysql mysql -umall -pmall123 fullstack_mall \
-e "SELECT id, payment_no, order_id, status_code, callback_id FROM mall_payment ORDER BY id DESC LIMIT 5;"
8.2 Redis Is the Performance Layer
Product detail cache:
docker exec fullstack-mall-redis redis-cli GET mall:product:detail:v1:1
docker exec fullstack-mall-redis redis-cli TTL mall:product:detail:v1:1
docker exec fullstack-mall-redis redis-cli DEL mall:product:detail:v1:1
If MySQL has the new value but Redis has the old value, the problem is in cache invalidation or TTL; if MySQL also has the old value, the problem is in the write chain.
flowchart TD
A[Page Shows Old Product Detail] --> B[Check MySQL]
B --> C{Is MySQL New Value?}
C -->|No| D[Troubleshoot Write API and Transaction]
C -->|Yes| E[Check Redis key]
E --> F{Is Redis Old Value?}
F -->|Yes| G[Troubleshoot evict and TTL]
F -->|No| H[Troubleshoot Response Assembly or Frontend Cache]
9. Understanding Business Rules Through Tests
The test names in this project are themselves business documentation. For example:
| Test Class | Rule Expressed by Test Name |
|---|---|
ProductFacadeCacheTest |
Cache hit does not query DB, MISS backfills, Redis degradation still queries MySQL, creation and status changes delete cache |
ProductDetailCacheServiceTest |
Bad JSON deletion self-heals, null value cache restores business code, write or delete failure is fail-open |
OrderControllerTest |
Duplicate idempotency key returns original order, cancellation releases inventory only once, regular user cannot read others' orders |
PaymentControllerTest |
Callback requires secret, duplicate callbacks are idempotent, late callback after cancellation is rejected, timeout order closure releases inventory |
PaymentMySqlContainerTest |
Concurrent payment creation results in only one transaction record, callback and closure race has only one consistent winner |
PaymentTransactionRollbackTest |
When inventory confirmation or release fails, order and payment transaction roll back together |
Frontend developers often treat tests as "something to run before committing." Backend developers should treat tests as executable business rules. When you are unsure how a boundary case should be handled, first look for the test name; if there is no test, add a minimal reproduction test.
10. Common Pitfalls and Fix Directions
10.1 Assuming a 404 Means the Controller Doesn't Exist
There are two types of 404: NOT_FOUND for a non-existent URL, and PRODUCT_NOT_FOUND / ORDER_NOT_FOUND for a non-existent business object. Check the business code first.
10.2 Refreshing the Token Upon Seeing a 403
403 is not an expired Token; it's insufficient permissions. Refreshing the Token won't turn a regular user into an admin. Check the account role and URL permission rules.
10.3 Only Looking at message, Not code
The Chinese message can change. Frontend logic should branch based on the stable code. Backend troubleshooting should also record the code.
10.4 Reproducing Complex Issues on the Page
Page reproduction is affected by too many factors. First, minimize the backend problem using curl, then return to the page.
10.5 Forgetting to Check Request Headers
JWT, Idempotency-Key, X-Mock-Payment-Secret are all in the request headers. Many order and payment problems are not due to a wrong body, but missing or incorrect headers.
10.6 Assuming Data in Redis Is the Latest
Redis might hold an old cache. Product details are based on MySQL. When troubleshooting old values, check both MySQL and Redis.
10.7 Ignoring Transactions and Concurrency
Order, payment, and inventory problems cannot be analyzed by looking only at single-threaded code. Look at @Transactional, row locks, conditional UPDATEs, unique indexes, and affected rows.
10.8 Local Profile and Test Environment Inconsistency
The local profile might use H2 and disable caching by default; the default profile connects to MySQL and Redis. When troubleshooting, first confirm the current startup parameters and environment variables.
10.9 Only Changing the Production SQL, Forgetting the Test SQL
Fields and table structures must be synchronized across the production schema, H2 test schema, MySQL container schema, and seed data. Otherwise, "works locally, fails in tests" is very common.
10.10 Not Saving the traceId
When the frontend reports a bug with only "the page errored," it's hard for the backend to locate. Including the traceId, URL, request body, time, and account greatly improves troubleshooting efficiency.
11. Three Typical Troubleshooting Cases
11.1 Public Detail Still Shows Old Value After Admin Modifies Subtitle
Troubleshooting order: First, use curl to directly call the modification API to confirm it's not a page issue where the request wasn't sent; second, check if the returned data.subtitle is the new value; third, check MySQL's mall_product.subtitle; fourth, check the Redis key mall:product:detail:v1:{id}; fifth, check logs for product_detail_cache event=EVICT or DEGRADED operation=EVICT; sixth, delete the Redis key and re-request the public detail.
If the public detail becomes new after deleting Redis, it indicates a cache invalidation problem; if it's still old, it indicates a Response assembly or database write problem; if MySQL itself is old, go back to ProductDbService.updateSubtitle to check the affected rows.
11.2 Duplicate Inventory Deduction When Creating an Order
Troubleshooting order: Check if the request header Idempotency-Key is the same; check if mall_order.idempotency_key has a unique constraint; check if order items are duplicated; check the SKU's available_stock and locked_stock; see if OrderControllerTest.sameIdempotencyKeyReturnsOriginalOrderWithoutLockingAgain covers this; if it's a concurrency issue, look at the MySQL container test and the conditional UPDATE in SkuMapper.lockStock.
11.3 Payment Success and Timeout Order Closure Race for the Same Order
Troubleshooting order: Check the order status_code, payment status_code, payment callback_id, order expires_at; check if the callback secret is correct; see whose conditional UPDATE in markPaid and closeExpiredOrder has an affected row count of 1; check if the inventory locked_stock was confirmed or released; refer to PaymentMySqlContainerTest.callbackAndTimeoutCloseHaveOneConsistentWinner.
flowchart LR
A[Complex Transaction Problem] --> B[Check Order Status]
B --> C[Check Payment Status]
C --> D[Check Inventory Fields]
D --> E[Check Conditional UPDATE Affected Rows]
E --> F[Cross-reference Concurrency Test]
12. Recommended Backend Troubleshooting Checklist
Every time you encounter a backend API problem, follow this checklist:
- Reproduce with curl first, don't rely on the page;
- Record the URL, HTTP method, request headers, request body;
- Check the HTTP status;
- Check
ApiResponse.code; - Save the
traceId; - Determine if the Controller was entered;
- If not entered, troubleshoot Security, path, JSON, validation;
- If entered, troubleshoot the Facade business branch;
- For write operations, check transactions and database affected rows;
- For reading old values, check MySQL first, then Redis;
- For transaction problems, check the three sets of fields: order, payment, inventory;
- Find the corresponding test name to understand expectations;
- If there is no test, add a minimal reproduction;
- After fixing, verify both the success path and the failure path;
- Update documentation or notes to avoid stepping on the same pitfall next time.
13. Chapter Exercises
- Use curl to construct a 401, a 403, a 400, and a 404, and record the
codefor each response. - Add
X-Trace-Id: debugTrace001to a request and confirm it appears in both the response body and headers. - Manually delete the Redis product detail key, then request the product detail and observe the MISS and PUT logs.
- After modifying the subtitle, check MySQL and Redis separately to determine where the data became stale.
- Read
GlobalExceptionHandlerand explain the difference betweenVALIDATION_ERRORandMALFORMED_JSON. - Read
RestAuthenticationEntryPointandRestAccessDeniedHandler, and explain why 401 and 403 occur before the Controller. - Pick a test class and translate 5 of its test names into Chinese business rules.
- Design a test case name and expected status code for "a regular user cannot modify the subtitle."
14. Series Summary: The Real Capabilities to Migrate from Frontend to Full-Stack
From Chapter 1 to Chapter 16, you have seen that the backend is not a collection of annotations, but an engineering system centered around data, boundaries, and consistency. Frontend experience is not obsolete: component layering can be migrated to Controller / Facade / Service layering, TypeScript interfaces to Request / Response DTOs, React Query caching to Redis Cache Aside, route guards to Security authorization, and the Network panel to curl and unified response troubleshooting.
But the backend adds several core responsibilities: the database is the source of truth; permissions must be enforced on the server side; transactions must guarantee multi-table consistency; concurrency must be handled by database conditional updates, row locks, and unique indexes; caching is a performance layer, not a fact layer; external callbacks require authentication and idempotency; error responses must be stable; logs and traceId must support troubleshooting; tests must cover boundaries and races.
flowchart TD
A[Frontend Capabilities] --> B[HTTP and JSON]
A --> C[State Management]
A --> D[Component Layering]
A --> E[Network Debugging]
B --> F[Controller and DTO]
C --> G[Redis Cache Aside]
D --> H[Facade and Service Layering]
E --> I[curl, traceId, Logs]
F --> J[Backend Engineering Capabilities]
G --> J
H --> J
I --> J
J --> K[Database, Transactions, Permissions, Concurrency, Testing]
15. Suggestions for Further Study
After completing this series, it is recommended that you continue with 5 tasks:
- Add a small field yourself, such as a product main image URL, and follow the complete process from Chapter 15 to modify it;
- Add a read-only API yourself, such as querying the number of products by category, to practice Controller, Facade, DbService, and testing;
- Complete the automated tests for the subtitle API, covering admin success, regular user 403, empty value 400, non-existent product 404, and cache invalidation;
- Manually create bad JSON in a real Redis instance and observe the self-healing;
- Design a "Favorite Product" module: first write the table structure and API contract, then implement the business logic.
16. Chapter Summary
The core of backend debugging is not guessing, but layered localization. First, fix the request with curl, then check the HTTP status and business code, then correlate logs with traceId, and then narrow the scope layer by layer along Controller, Facade, DbService, Mapper, MySQL, Redis, and tests. The biggest advantage for a frontend engineer transitioning to the backend is already being familiar with HTTP, JSON, state, and debugging tools; the biggest challenge is to supplement the backend responsibilities of database as the source of truth, permission boundaries, transaction consistency, concurrency control, and cache degradation.
If, when encountering a problem in the future, you can naturally say, "First check the code and traceId, then check if MySQL really changed, then check if Redis has the old value, then find a test to reproduce it," it means you have started to shift from "a frontend developer who can call APIs" to "a full-stack engineer who can locate and fix backend chains."
Appendix A: Categorizing Errors into 5 Layers
The worst thing in backend troubleshooting is a vague "the API doesn't work." You must categorize errors into layers. The first layer is the network layer: is the port started, does the URL reach the backend, are Docker dependencies healthy? If curl cannot connect to localhost:8080, you should not look at business code but check if the service is started, if the port is occupied, and if the profile is correct.
The second layer is the protocol layer: are the HTTP method, path, request headers, Content-Type, and JSON format correct? For example, using GET to call a PATCH API, or forgetting Content-Type: application/json, the problem occurs before the business logic.
The third layer is the security layer: is the JWT valid, does the role meet requirements, is the callback secret correct, does the idempotency key match the format? 401, 403, PAYMENT_CALLBACK_SECRET_INVALID all belong to this layer.
The fourth layer is the business layer: is the product listed, is the inventory sufficient, does the order status allow cancellation, is the payment still PENDING, is the category enabled? This layer usually returns 404, 409, or specific business codes.
The fifth layer is the infrastructure layer: MySQL connection, Redis connection, transaction rollback, SQL constraints, old cache values, scheduled tasks, external callback concurrency. 500, DEGRADED logs, and Testcontainers failures usually fall into this layer.
flowchart TD
A[API Does Not Meet Expectations] --> B[Network Layer]
B --> C[Protocol Layer]
C --> D[Security Layer]
D --> E[Business Layer]
E --> F[Infrastructure Layer]
B --> B1[Service, Port, Docker Health]
C --> C1[Method, Path, Header, JSON]
D --> D1[JWT, Role, Secret, Idempotency Key]
E --> E1[State Machine, Inventory, Permission Ownership]
F --> F1[MySQL, Redis, Transactions, Scheduled Tasks]
Troubleshoot from top to bottom every time, don't skip. Many 401 problems are useless to debug by looking at business code for 2 hours because the request never entered the Controller; many old value problems are useless to fix by changing the frontend state 5 times because the old JSON is in Redis.
Appendix B: How to Read Logs, and How Not To
Reading logs is not about getting lucky in a sea of text, but searching with a traceId and keywords. The current project's product cache logs have fixed patterns: product_detail_cache event=HIT, MISS, PUT, PUT_NULL, EVICT, DEGRADED, CORRUPTED. Although payment and order logs are not as centralized as cache logs, you can still locate issues through business codes, traceId, SQL results, and test reproduction.
Don't just look at the last line of the exception. The last line is usually just the exception type; the real cause might be in the first Caused by block above. Also, don't treat every warn as a critical error. For example, Redis DEGRADED operation=PUT indicates a cache write failure, but the current product detail request might still succeed; this requires attention to Redis health, but doesn't mean the business data is wrong.
Frontend developers are often used to using console.log to check variables. The backend can also use breakpoints, but logs are more suitable for online and asynchronous scenarios. Good logs should include the event name, key business IDs, status, traceId, and exception. Bad logs only say "entered method" or "errored out" without context, offering little troubleshooting value.
If you add logs for orders or payments in the future, it is recommended to write them in a structured style:
order_payment event=CALLBACK_SUCCESS orderId=10 paymentNo=PAY-xxx statusBefore=PENDING statusAfter=SUCCESS traceId=abc123
This way, when the frontend provides a traceId or order number, the backend can search quickly.
Appendix C: How to Turn an Online Problem into a Test
Suppose you encounter "duplicate clicks on create order cause inventory to be locked twice." Don't just fix it manually. You should turn it into a test: prepare a shopping cart, use the same Idempotency-Key to request order creation twice consecutively, assert that the same order is returned, assert that the SKU's available_stock decreases only once, and locked_stock increases only once. The current project already has a similar test: sameIdempotencyKeyReturnsOriginalOrderWithoutLockingAgain.
Suppose you encounter "bad JSON in Redis causes product detail to always return 500." You should write a test: manually make the Redis mock return illegal JSON, call lookup, assert the status is MISS, assert that delete was called, and assert that no exception was thrown upward. The current project's ProductDetailCacheServiceTest.shouldDeleteBrokenJsonAndTreatItAsMiss follows this thinking.
Suppose you encounter "payment callback and timeout order closure happen simultaneously, causing chaotic order status." You should use a concurrency test or MySQL container test to reproduce the race, have two threads execute the callback and closure simultaneously, and finally assert that the order, payment, and inventory are consistent. The current project's PaymentMySqlContainerTest.callbackAndTimeoutCloseHaveOneConsistentWinner already covers this boundary.
Turning a bug into a test has 3 benefits: first, it proves you truly understood the problem; second, it prevents future refactoring from breaking it again; third, it allows later learners of this project to understand the business rules directly through the test names.
Appendix D: How to Set Backend Debug Breakpoints
If you are debugging in IDEA, it is not recommended to set breakpoints everywhere at first. Set key breakpoints along the chain. A Controller breakpoint is used to confirm whether the request entered and whether parameters were bound successfully. A Facade breakpoint is used to see the current user, business branch, state machine judgment, and cache status. A DbService breakpoint is used to see query conditions, UPDATE affected rows, and exception conversion. Mapper or SQL logs are used to see the final database operations. An ExceptionHandler breakpoint is used to see how exceptions are converted into responses.
Taking the subtitle API as an example, the breakpoint order is: ProductAdminController.updateSubtitle to see id and request.subtitle; ProductFacade.updateSubtitle to see the trimmed value; ProductDbService.updateSubtitle to see the wrapper conditions and affectedRows; ProductDetailCacheService.evict to see if the cache was deleted; ProductFacade.toDetailResponse to see the returned fields; finally, the curl response to see data.subtitle.
Taking the product detail cache as an example, the breakpoint order is: ProductController.detail, ProductFacade.getPublishedProduct, ProductDetailCacheService.lookup, RedisOperatorClient.get, ProductDbService.requirePublishedById, ProductDetailCacheService.put. If the cache hits, the DbService breakpoint should not be entered; if Redis DEGRADED, the DbService should be entered.
Taking the payment callback as an example, the breakpoint order is: MockPaymentCallbackController.callback, PaymentFacade.requireValidCallbackSecret, PaymentCallbackTransactionService.handleSuccess, PaymentDbService.selectByPaymentNoForUpdate, OrderDbService.markPaid, PaymentDbService.markSuccess, SkuDbService.confirmLockedStock. If it's a duplicate callback, it should return directly in the already SUCCESS branch without confirming inventory again.
Appendix E: A Verbal Template for Frontend Developers to Report Backend Issues
When you describe a backend problem to someone else, you can use this template:
"I reproduced the problem with curl. The request is METHOD + URL, the request headers contain these key headers, the request body is this. The HTTP status is X, the response code is Y, the traceId is Z. Whether the Controller was entered. After entering, which branch the Facade took. What the current key MySQL fields are. What the current Redis key is. Whether existing tests cover this scenario; if not, what test I plan to add."
This template can shift communication from "it seems broken" to "evidence-driven." For example:
"I used curl to call PATCH /api/admin/products/1/subtitle, the admin token is correct, HTTP 200, code SUCCESS, traceId is debugSubtitle001. MySQL's subtitle is already the new value, but GET /api/products/1 still returns the old value. The Redis key mall:product:detail:v1:1 still contains the old JSON, and the logs do not show EVICT, so the problem is in the post-write cache invalidation chain."
With a description like this, a backend developer will immediately know to look at ProductFacade.updateSubtitle and ProductDetailCacheService.evict, rather than suspecting the frontend page.
Appendix F: How to Continue Practicing Backend After This Series
If you have finished reading all 16 chapters, don't stop at "understanding." Backend ability is consolidated through hands-on practice. It is recommended to do 3 progressive exercises.
Exercise 1: Add tests for the subtitle API. You will become familiar with MockMvc, login tokens, JSON assertions, database assertions, and cache mocking. This exercise is small but complete, very suitable for getting started.
Exercise 2: Add a product main image field coverImageUrl. You need to modify the SQL, Entity, Request, Response, Facade, Controller, cache invalidation, and documentation. It is similar to the subtitle but can include URL format validation, training you to abstract the same development process.
Exercise 3: Add a "Favorite Product" module. It will involve user-private resources, unique indexes, paginated queries, unfavoriting, permission isolation, and testing. You will practice the "current user + data ownership" thinking from the shopping cart and orders again.
For each exercise, follow this process: first write the requirements and API contract, then modify the database, then write the minimal implementation, then write tests, then verify with curl, then supplement the documentation. Don't start by writing the Controller. The most important things in backend engineering are a sense of order and boundaries.
Appendix G: Writing a Troubleshooting Session into a Team-Reusable Record
Knowing how to debug is only the first step. A higher-level ability is to write the troubleshooting process into a record that others can reuse next time. It is recommended that every time you encounter a typical problem, you document it using a fixed template: problem phenomenon, impact scope, reproduction steps, key request, HTTP status, business code, traceId, related logs, database evidence, Redis evidence, root cause, fix plan, supplementary tests, preventive measures. This template looks long, but it becomes very fast once you are familiar with it, because it follows the natural chain of backend problems.
For example, "public detail not updating after admin modifies subtitle": the phenomenon is the public detail still returns the old subtitle; the reproduction steps are first request the detail to create a cache, then call the admin modification, then request the detail again; the key requests are PATCH /api/admin/products/{id}/subtitle and GET /api/products/{id}; the database evidence is mall_product.subtitle has been updated; the Redis evidence is mall:product:detail:v1:{id} still holds the old JSON; the root cause might be that the write API missed evict; the fix plan is to delete the product detail cache after the database update succeeds in ProductFacade.updateSubtitle; the supplementary test is a cache invalidation test. Written this way, team members don't need to guess again.
Frontend developers can also analogize this to online bug post-mortems. The difference is that frontend post-mortems often revolve around the browser, component state, and user operations, while backend post-mortems need to pay more attention to the request chain, data state, and concurrency timing. Don't just send a "fixed" message in the chat tool. A fix without an evidence chain is very likely to cause the same mistake again on a different API next time.
Appendix H: Why Backend Troubleshooting Must Prioritize Isolating Variables
Many frontend developers who have just transitioned to the backend like to change the page, change the API, restart the service, and clear Redis all at once. In the end, the problem is resolved, but they don't know which action worked. The most important method in backend troubleshooting is isolating variables: change only one condition at a time and observe if the result changes. First, use curl to bypass the frontend page and confirm the API itself; then fix the request body to confirm the parameters; then fix the database data to confirm the business rules; then temporarily disable caching to confirm if Redis is related; finally, return to page integration testing.
Take an order problem: the page prompts insufficient inventory. Don't immediately change the button logic, and don't immediately suspect the transaction. First, use curl with the same JWT and the same Idempotency-Key to reproduce; then check if the selected shopping cart items exist; then check the SKU's available_stock; then check the lockStock affected rows; then confirm if it's returning an old order due to a duplicate idempotency key. Each step verifies only one hypothesis. If you simultaneously change the user, product, clear the cart, and modify the inventory, you turn the problem into a random phenomenon.
Isolating variables also helps you communicate with backend colleagues. You can say: "I have bypassed the page and reproduced it with curl; for the same product id, when Redis has the old detail, it returns the old subtitle; after deleting the key, it returns the new subtitle." This statement is ten times more valuable than "the page seems to have a caching problem" because it directly narrows the scope to cache invalidation.
Appendix I: Migrating from Frontend DevTools to the Backend Toolbox
Frontend troubleshooting commonly uses DevTools' Network, Console, Application, and Performance panels. The backend has corresponding tools. Network corresponds to curl, Postman, API tests, and gateway logs; Console corresponds to application logs, exception stacks, and traceId; localStorage/cookies in Application correspond to JWT, Redis, and database state; Performance corresponds to API latency, slow SQL, cache hit rate, and thread pool status. You don't need to master all backend tools at once, but you should know the "counterpart" of each frontend tool in the backend world.
The recommended minimal toolbox for this project is: curl to view the HTTP contract, jq to format JSON, mysql or a database client to see the source of truth, redis-cli to see the cache, IDEA Debug to see the call stack, mvn test for automated verification, and logs to see the traceId. Don't get obsessed with complex APM platforms at the beginning, because the most important thing for a local learning project is understanding the chain. When you can reproduce a problem without the page, using only curl and the database, your backend debugging ability has clearly surpassed the stage of "only knowing how to look at page errors."
When using tools, also pay attention to the environment. The key you check in local Redis does not necessarily exist in the test environment; disabling caching in the dev profile does not mean production also disables it; your local database seed data might be different from someone else's machine. Backend problems are often not the code itself, but a combination of configuration, data, and environment. Therefore, every time you report a problem, include the environment: local / test / production, profile, database version, whether Redis is started, the current logged-in user role, request headers, and request body.
Appendix J: A Graduation Exercise Set for Readers of This Series
If you want to test whether you have truly completed the first stage of "frontend to full-stack," you can do a set of graduation exercises on this project. Question 1: Add a "Favorite Product" API: users can favorite, unfavorite, and view their own favorites list. You need to design the table, Controller, Facade, DbService, permissions, unique constraints, and tests. The key point is that userId must come from the JWT and cannot trust the frontend to pass it as a parameter. Question 2: Add a "Product Review" API: only paid orders can review, duplicate reviews must be rejected, and the public detail can display a review summary. The key points are cross-table validation and status constraints. Question 3: Add an image list to the product detail: you need to consider Response structure changes, cache key versions, backend upload, and public reading.
When doing these exercises, don't rush to write code. First, draw diagrams using the methods from this series: request chain diagram, table relationship diagram, state machine diagram, exception return diagram, cache invalidation diagram. Then list the file checklist: SQL, Entity, Mapper, DbService, Facade, Controller, Request, Response, Security, Tests. Finally, write acceptance scripts with curl. You will find that backend development is not mysterious magic, but a repeatable engineering process.
True full-stack ability is not "a frontend developer who can also write a bit of Java," but understanding how, behind a single user operation, the browser, HTTP, authentication, business rules, transactions, database, cache, logs, and tests work together to ensure system correctness. This series has already taken you through the core closed loop of a mall: products, SKU, shopping cart, orders, payments, caching, backend modifications, and debugging. For every new module you build next, you can reuse the same set of thinking.
Appendix K: How to Avoid "Guess-Driven" Backend Troubleshooting
Guessing is not a bad thing, but guesses must be verified by evidence. Many problems initially have multiple possible causes: wrong API path, incorrect permissions, parameter validation failure, business status not allowed, database data mismatch, old Redis value, transaction rollback, concurrency conflict, different test environment configuration. Good troubleshooting is not picking the most likely cause and modifying it directly, but listing the possible causes and eliminating them one by one with the cheapest evidence.
For example, when seeing a 403, don't immediately say "Token expired." First, check the HTTP status and business code; if it's 401, it's more like unauthenticated; if it's 403, it likely means authenticated but insufficient permissions. Then check if the request path hits /api/admin/**, then check if the current user role is ADMIN, then check the rules in SecurityConfig. No step in this process relies on guessing; it's all evidence.
The same goes for seeing a 404. First, distinguish whether it's a Spring routing 404 or a business object 404. If the response body is still a unified ApiResponse and the code is PRODUCT_NOT_FOUND, then the Controller actually entered; if there is no unified response structure at all, it's more likely a non-existent path or mismatched request method. Many frontend developers treat all 404s as a wrong URL, because in frontend routing, 404 often means a page doesn't exist, but a backend 404 can mean either a route doesn't exist or a resource doesn't exist.
Appendix L: Using This Article Series as a Backend Learning Map
After reading all 16 chapters, you can use a map to review your abilities. Chapters 1 to 3 let you know the project structure, startup process, and Controller contract; Chapters 4 to 5 let you understand MySQL and MyBatis-Plus; Chapters 6 to 7 supplement JWT, Spring Security, and Redis; Chapters 8 to 13 enter the core mall business, from product details, state machines, SKU, shopping cart, orders to payments; Chapters 14 to 15 tie together caching and real-world requirement development; Chapter 16 is responsible for consolidating debugging methods into a reusable process.
In the future, when you learn any backend framework, you can migrate using this map. Switching to Node.js, Go, Python, or other Java frameworks, the themes of HTTP, authentication, databases, caching, transactions, idempotency, logging, and testing still exist. Framework APIs will change, but engineering thinking will not. This is also the most important source of confidence for transitioning from frontend to full-stack: you are not starting from scratch, but building upon your existing componentization, state management, API integration, and engineering skills, supplementing the server-side capabilities of data consistency and system boundaries.
Appendix M: Troubleshooting Habit Advice for Your Future Self
First, for any API problem, save the original request and original response first, don't just screenshot the page. The request method, URL, request headers, request body, status code, and response JSON are all evidence. Second, every time you see a unified response body, read the code and traceId first, don't just read the message. Third, when encountering data inconsistency, first determine the source of truth: orders look at the order table, inventory looks at the SKU table, product details look at the product table; caching can only be used as a reference for the acceleration layer. Fourth, when encountering permission problems, first distinguish between 401 and 403. Fifth, when encountering duplicate submissions, prioritize checking the idempotency key, unique index, and transaction boundaries.
Sixth, don't be afraid to read tests. Tests are often closer to business rules than the implementation, because they describe the system's promises using inputs and outputs. Seventh, don't hand all problems to the frontend for reproduction. As long as it can be reproduced with curl, the problem is detached from page complexity. Eighth, after fixing, definitely add a test that can fail and then turn green, otherwise you've only temporarily fixed it once. Ninth, when writing logs, serve troubleshooting; don't just print "entered method." Tenth, stay humble: backend problems often come from the superposition of multiple factors; an evidence chain is more reliable than intuition.
A final reminder: troubleshooting ability is not about memorizing commands, but establishing an order. First confirm the entry point, then confirm the identity, then confirm the parameters, then confirm the business rules, then confirm the data, then confirm the cache and concurrency. When the order is stable, the problem becomes smaller.