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)