Why this stack, honestly
Spring Boot and React are not chosen because they are exciting. They are chosen because both have deep hiring pools, long support horizons, and enormous amounts of prior art for the boring problems — authentication, pagination, file upload, background jobs. On an enterprise project, the cost of a rare stack is paid every time someone leaves.
The trade is that neither is the fastest thing to prototype in. If the goal is a weekend proof of concept there are quicker paths. If the goal is a system a rotating team maintains for five years, predictability beats novelty, and this pairing is about as predictable as it gets.
Layer the backend so the domain does not depend on the framework
Controllers handle HTTP and nothing else: parse, validate, delegate, serialise. Services hold business logic and know nothing about HTTP. Repositories handle persistence. This is unremarkable until you see what happens without it — business rules embedded in controllers, impossible to test without spinning up the web layer and impossible to reuse from a scheduled job.
The rule I enforce is that domain objects never cross the HTTP boundary. Explicit request and response DTOs, mapped deliberately. It feels like duplication on day one and it is what lets you change a database column without breaking every API consumer, or add an API field without touching the persistence model.
REST or GraphQL, and how to decide
REST is the default and should be. It is cacheable, debuggable with curl, and every developer and tool already understands it. GraphQL earns its place when you have several distinct clients wanting materially different shapes of the same data, and the alternative is a proliferation of bespoke endpoints.
What GraphQL costs is a new set of concerns most teams underestimate: query depth limiting, complexity budgets, and the N+1 problem, which arrives immediately and requires batching to solve. If you have one web client and one mobile client with similar needs, REST plus a couple of purpose-built aggregate endpoints is less machinery for the same outcome.
Frontend structure and server state
The most consequential frontend decision is separating server state from client state. Data fetched from the API is a cache of something owned elsewhere, with its own concerns — staleness, refetching, deduplication. Client state is genuinely local: form contents, which modal is open. Putting server data in a global client store forces you to reimplement caching badly.
Organise by feature rather than by technical type. A folder per feature holding its components, hooks and API calls means a change to one area touches one folder. The alternative — components, hooks and services as top-level directories — means every feature is smeared across the tree and every change is a scavenger hunt.
The contract between the two halves
The API contract should be generated, not transcribed. Publishing an OpenAPI schema from the backend and generating typed clients for the frontend eliminates an entire category of bug: the field renamed on the server and missed in three places on the client. Hand-written interfaces mirroring the backend drift, quietly, and the drift is found by users.
Version additively. New fields are safe, removed or renamed fields are not. When something must break, run both shapes for a deprecation window and use the API logs to confirm nobody is still calling the old one. That requires per-consumer usage logging, which is worth adding before you need it.
Build, containerise, deploy
One pipeline: build, unit test, integration test against a real database in a container, build the image, push, deploy. Integration tests against a throwaway real database rather than an in-memory substitute are worth the extra seconds, because the substitute's SQL dialect differs in exactly the places that break in production.
Multi-stage Docker builds keep the runtime image small — build dependencies do not belong in the thing you ship. Pin base image versions and rebuild regularly for patches. And make the frontend build environment-agnostic, with runtime configuration injected rather than baked in, so one artefact promotes cleanly from staging to production.
Key takeaways
- Choose this stack for hiring pool and prior art, not excitement — predictability is the enterprise feature
- Keep business logic out of controllers and never let domain objects cross the HTTP boundary
- Default to REST; adopt GraphQL only for genuinely divergent clients, and budget for depth limits and N+1
- Separate server state from client state on the frontend, and organise folders by feature
- Generate typed API clients from an OpenAPI schema instead of hand-writing interfaces that silently drift
- Integration-test against a real containerised database, and inject frontend config at runtime so one artefact promotes
Conclusion
The architecture that scales is not the clever one, it is the one a new engineer can navigate in a week. Clear layers, generated contracts, feature-organised frontend, and a pipeline that tests against something resembling production. The trade-offs are real but they are known, which is the whole point.
Enjoyed this article?

Vivek Kumar Singh
Technical Expert · Full Stack Cloud Engineer · Tokyo, Japan