Next.js 16 + Spring Boot - our production architecture
Static Next.js frontend, Spring Boot backend, embedded admin SPA. A real-world layout from apps/site: why not a monolith, and how it actually ships.
Static Next.js frontend, Spring Boot backend, embedded admin SPA. A real-world layout from apps/site: why not a monolith, and how it actually ships.
Most projects I work on end up with the same layout: static Next.js frontend + JVM Spring Boot backend + an embedded admin SPA. This isn't a first-pass decision - it's the result of several iterations. Here's what works and what I'd change if I were starting today.
The itsolutions-platform repo is a pnpm workspace with three apps:
apps/site - the public site. Next.js 16 with output: 'export'.apps/site/admin-frontend - the admin SPA (React Router, Vite). Embedded in site.apps/site/backend - Spring Boot 3, PostgreSQL, JWT auth.This isn't ideological. It's three genuinely different use cases:
| App | Traffic profile | Tech decision |
|---|---|---|
| Public site | Long-tail SEO, 95% one-time visitors | Static export → Caddy → CDN |
| Admin | 1 user, 100 visits/day | SPA, no SSR - fast dev cycle |
| Backend | Steady TPS, transactions, emails | JVM + JPA + scheduler |
Each component has a different bar for uptime, cold start, and deploy frequency. Forcing them into a Next.js monolith would mean compromising everywhere a little.
Asked weekly. Reasons I'm sticking with the JVM:
output: 'export' rules out server logic. The site is served by
Caddy with a CDN - no Node runtime spins up per request. A form lead
POSTs to api.itsolutionsdz.pl, not to Next.js.@Scheduled,
Spring Mail, Spring Retry. I don't want to wire retry logic into Vercel
CRON handlers by hand.The admin SPA is embedded in site:
apps/site/admin-frontend/ ← React Router, Vite
apps/site/public/admin/ ← build output, copied on `pnpm build`
apps/site/middleware.ts ← gates /admin/* with basic auth
After login, admin holds a JWT in an httpOnly cookie. Every backend call
goes through fetch('/api/...') - Caddy proxies that prefix to Spring Boot
on a separate port. Cookie is SameSite=Lax, so CSRF needs its own token,
but in return admin lives on the same domain as the public site without
CORS gymnastics.
The stack lives on one VPS:
Deploy is docker compose -f docker-compose.prod.yml up -d --build.
Static out/ is served by Caddy with no Node in the middle. Backend and
Postgres run in containers with restart policies.
“Architecture should be a result of project requirements, not a list of conference-stage tooling.
”
Three apps in a monorepo, each with its own deploy target, mean I can change the site's hero section without redeploying the backend. I couldn't pull that off in full-stack Next.js.