Web Application large-enterprise hybrid architecture
High Level Architecture – On Prem

Layer-by-layer description
F5 ASM (WAF) is the first thing the internet touches. It inspects all HTTP/S traffic for OWASP Top 10 attacks (SQL injection, XSS, CSRF), enforces DDoS rate limits, checks IP reputation lists, detects bot traffic, and handles SSL termination or re-encryption. It never forwards malformed or flagged requests deeper into the stack.
Nginx (load balancer + reverse proxy) receives clean traffic from F5 and distributes it across multiple backend instances. It runs health checks, supports weighted routing for blue/green deploys, handles sticky sessions where needed, and routes based on URL path — /static/* goes to Apache, /api/* goes to Spring Cloud Gateway.
Apache HTTP server only exists to serve the pre-built React artefacts. On first load the browser receives index.html, the JS bundle, and CSS. After that, Apache is out of the picture — the React app runs entirely in the browser.
React SPA (browser) uses MSAL.js to redirect the user to Azure AD’s login page. After successful authentication, Azure AD issues an access token (JWT). Every subsequent API call includes this token in the Authorization: Bearer header. The SPA calls the Spring Cloud Gateway directly over HTTPS.
Azure AD acts as the central identity authority. You register both the React app and the backend APIs as App Registrations. The React app gets delegated permissions; the gateway validates tokens using Azure AD’s JWKS public keys. Role claims in the JWT (roles: ["admin", "viewer"]) flow through to microservices for fine-grained authorisation.
Spring Cloud Gateway is the internal API entry point for all microservice traffic. It validates every JWT (rejecting expired or tampered tokens), enforces route predicates (which path maps to which service), applies cross-cutting filters (add correlation ID header, strip internal headers, log access), and provides circuit breakers per downstream service. Rate limiting here prevents a single client from overwhelming any one microservice.
Service mesh (Istio/Linkerd) wraps all inter-service communication in mutual TLS automatically, so services can’t impersonate each other. It provides retries with backoff, circuit breakers, and — critically — produces the spans that distributed tracing needs without any code changes in the services themselves.
Microservices each own a bounded domain. They communicate synchronously (REST/gRPC via the mesh) for low-latency request/response, and asynchronously (Kafka events) for cross-domain workflows like order.created → payment.process → stock.reserve → notification.send.
Database-per-service is the key data isolation pattern. No service reads another’s database directly. Cross-service data needs are satisfied by event consumption or API calls. PostgreSQL suits most transactional services; Redis suits the notification queue; Elasticsearch is added to product for full-text search.
Shared infrastructure layer — Azure Blob Storage handles file uploads and static assets; Azure Front Door/CDN edge-caches the React bundle globally; Azure Key Vault holds all secrets (DB passwords, API keys), injected at runtime via Spring Cloud Config or environment variables — never baked into Docker images.
Observability is non-negotiable in a distributed system. Jaeger/Tempo gives distributed tracing across all service hops (correlated by the X-Correlation-ID the gateway injects). Prometheus scrapes metrics from every service; Grafana dashboards them. ELK or Azure Monitor centralises structured logs. Without these three, debugging a failed request that touched five services is nearly impossible.