DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

I'm Building a Production-Grade Spring Boot + React App, One Feature a Day (Day 1)

I just finished a 50-day "new tech every day" series. For the next 50 days I'm doing the opposite: ONE production-grade app, one feature a day — and building both the Spring Boot backend AND the React frontend each day. This is Day 1.

The app: OrderHub, an e-commerce order-fulfillment backend that will grow into a real event-driven microservices system (Redis → Kafka → sagas → Kubernetes). But today it starts where every solid service starts: a clean REST API with a proper layered architecture.

🌐 Live UI: https://frontend-pied-six-23.vercel.app
👉 Repo (read the commits in order): https://github.com/dev48v/order-hub-from-zero

The backend: layers, one job each

HTTP → Controller → Service → Repository → Domain
        thin        rules      interface    model
Enter fullscreen mode Exit fullscreen mode

Each layer only talks to the one below it. That separation is the single most important thing for a codebase that has to survive 50 days of new features — you change one layer without breaking the others.

Controller stays thin — translate HTTP, nothing else:

@PostMapping
ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest r) {
  var o = service.placeOrder(r.customer(), r.item(), r.quantity());
  return ResponseEntity.created(URI.create("/api/orders/" + o.getId()))
                       .body(OrderResponse.from(o));
}
Enter fullscreen mode Exit fullscreen mode

DTOs separate the API from the domain — and validate at the boundary:

public record CreateOrderRequest(
    @NotBlank String customer,
    @NotBlank String item,
    @Positive int quantity) {}
Enter fullscreen mode Exit fullscreen mode

@Valid means a bad body is a 400 with zero code from me.

The repository is an interface — today an in-memory ConcurrentHashMap implements it; on Day 2 Spring Data JPA implements it against Postgres, and the service/controller/tests don't change a line:

public interface OrderRepository {
  Order save(Order o);
  Optional<Order> findById(String id);
  List<Order> findAll();
}
Enter fullscreen mode Exit fullscreen mode

Constructor injection wires it all together (no @Autowired in modern Spring), which also makes every class trivially unit-testable.

The frontend: React 19, modern stack

The same day ships the UI: React 19 + Vite + TypeScript + Tailwind v4, shadcn-style components. The key piece is one API module with a mock fallback:

export const api = {
  listOrders: () => USING_MOCK ? mock.list() : http('/api/orders'),
  placeOrder: (r) => USING_MOCK ? mock.place(r)
                    : http('/api/orders', { method: 'POST', body: JSON.stringify(r) }),
}
Enter fullscreen mode Exit fullscreen mode

If VITE_API_URL points at the deployed backend, it calls the real API; if not, it falls back to in-memory data. That's why the live Vercel demo works even while a free-tier backend is cold-starting — the UI never breaks.

Types mirror the Java DTOs so the front and back ends can't silently drift, and the data flow is plain hooks: useEffect loads on mount, handlers place/confirm and refetch.

Why this format

A daily "new tech" series is fun but it doesn't compound — each day is disconnected. Building ONE app feature-by-feature does: by Day 50 there's a real, deployable, event-driven system you can read commit by commit. And doing BE + FE together every day is how you actually ship.

Day 2: persist orders with JPA + PostgreSQL (watch the in-memory repository get swapped with nothing above it changing).

Follow along — repo + live UI linked above. 🚀

Top comments (0)