From Chaos to Production: Building a Real Food-Delivery Backend with Spring Boot

The article chronicles the step‑by‑step evolution of a small team’s chaotic food‑delivery backend into a production‑ready system, detailing how they introduced layering, transactions, security, caching, async processing, messaging, observability, scalability, resilience, containerization, and testing using Spring Boot, Kafka, Redis, JWT, and Docker.

LuTiao Programming
LuTiao Programming
LuTiao Programming
From Chaos to Production: Building a Real Food-Delivery Backend with Spring Boot

Stage 1 – Get Something Running

The initial implementation lives entirely in a controller with no layering, validation, or error handling, e.g.:

@RestController
@RequestMapping("/api")
public class OrderController {
    @PostMapping("/order")
    public String createOrder() {
        return "order created";
    }
    @GetMapping("/menu")
    public List<String> getMenu() {
        return List.of("Burger", "Pizza", "Drink");
    }
}

The system can place orders, but the code quickly becomes unmaintainable.

Stage 2 – Introduce Layering

As requirements grow (parameter validation, exception handling, new fields, new endpoints), business logic accumulates in the controller, causing global side‑effects and exponential debug cost. The team restructures the project into packages:

/com/icoderoad/order
├── controller
├── service
├── repository
├── model

Controller now delegates to a service:

@RestController
@RequestMapping("/orders")
public class OrderController {
    private final OrderService orderService;
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }
    @PostMapping
    public Order create(@RequestBody OrderRequest request) {
        return orderService.createOrder(request);
    }
}

@Service
public class OrderService {
    public Order createOrder(OrderRequest request) {
        // business logic
        return new Order();
    }
}

Dependency injection becomes a practical tool to avoid collapse.

Stage 3 – Ensure Data Consistency with Transactions

A production issue appears: an order is marked as completed even though payment failed. The root cause is non‑atomic database operations. The solution is to annotate the service method with @Transactional:

@Service
public class OrderService {
    @Transactional
    public void createOrder(OrderRequest request) {
        saveOrder();
        processPayment();
    }
}

This guarantees “all‑or‑nothing” behavior.

Stage 4 – Secure the System with Authentication

Another bug lets user A see user B’s orders because the client supplies the user ID. The team replaces client‑provided IDs with server‑side authentication, introducing a user model, role hierarchy (Customer/Restaurant), password hashing, and JWT tokens:

public String generateToken(User user) {
    return Jwts.builder()
        .setSubject(user.getUsername())
        .signWith(SignatureAlgorithm.HS256, secretKey)
        .compact();
}

All endpoints now rely on the token rather than a request parameter.

Stage 5 – Add Caching for Performance

Frequent menu queries cause repeated database hits. The team adds Spring Cache with Redis backing:

@Cacheable(value = "menu")
public List<MenuItem> getMenu() {
    return menuRepository.findAll();
}
spring:
  cache:
    type: redis

Performance improves because the bottleneck is a data‑access strategy, not raw CPU speed.

Stage 6 – Move to Asynchronous Processing

Synchronous payment simulation and email notification block the order thread, inflating response time. The code is refactored to use @Async for email sending:

@Async
public void sendEmailAsync() {
    // send email
}

Latency drops, but failures in the async task become invisible, highlighting a new reliability concern.

Stage 7 – Achieve Reliable Delivery with Messaging

Intermittent email failures and lack of retries lead to lost events. The team introduces Kafka:

kafkaTemplate.send("order-topic", orderEvent);
@KafkaListener(topics = "order-topic")
public void handle(OrderEvent event) {
    sendEmail(event);
}

Retry mechanisms, dead‑letter queues, and idempotent handling are added, giving the system reliable message delivery.

Stage 8 – Continuous Security Hardening

Token expiration, privilege escalation, and role abuse appear. The team adopts RBAC, token lifecycle management, and fine‑grained endpoint authorization, treating security as an evolving boundary.

Stage 9 – Build Observability

When orders fail without clear reasons, the team adds structured logging, Spring Boot Actuator, metrics, ELK stack, and Grafana:

management:
  endpoints:
    web:
      exposure:
        include: "*"

This enables queries such as orders per minute, slowest endpoint, and failing services.

Stage 10 – Design for Scalability

Faced with a potential ten‑fold traffic surge, the architecture evolves to include an API gateway and load balancer, shifting scalability from a post‑hoc patch to a design‑time concern.

Stage 11 – Add Resilience with Circuit Breakers

Payment service slowdown can cascade into an avalanche. A circuit‑breaker annotation protects the system:

@CircuitBreaker(name = "paymentService", fallbackMethod = "fallback")
public String callPayment() {
    return paymentClient.pay();
}

When failures occur, the call is short‑circuited, preventing system‑wide collapse.

Stage 12 – Containerize the Environment

The application is packaged with Docker Compose, defining services for the app, MySQL, Redis, and Kafka:

version: '3'
services:
  app:
    build: .
  mysql:
    image: mysql:8
  redis:
    image: redis
  kafka:
    image: bitnami/kafka

Now the system is a reproducible runtime environment rather than just source code.

Stage 13 – Establish a Test Suite

To verify correctness, the team adds unit tests, integration tests, and Cucumber BDD scenarios, e.g.:

Scenario: 用户下单
  Given 用户已登录
  When 提交订单
  Then 订单应创建成功

Technical and business concerns speak the same language.

Conclusion

The journey shows that Spring Boot, Kafka, Redis, and JWT are not learning goals themselves; they are pragmatic choices forced by emerging problems. The real skill lies in progressively imposing order on chaos, moving from writing code to building a robust, observable, and maintainable system.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Dockerbackend-architecturemicroservicesRedisKafkaSpring Bootrefactoring
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.