Section 20
HTTP Concurrency Model
One of the most important practical differences between Java Spring and Go is how they handle concurrent HTTP requests. Java has two distinct models — imperative (thread-per-request) and reactive (event loop). Go has one model — goroutines — which gives you the efficiency of reactive without the complexity.
The three concurrency models
Spring Boot MVC (imperative): Each HTTP request is handed a thread from Tomcat's thread pool. That thread is occupied — blocked — until the response is sent. Simple to reason about, but thread count limits throughput.
Spring WebFlux (reactive): A small number of Netty event-loop threads handle all requests using non-blocking I/O. Code must be written as reactive streams (Mono/Flux). High throughput, but a completely different programming model.
Go (net/http / Gin): Each HTTP request spawns a goroutine (~2KB stack). The Go runtime scheduler multiplexes goroutines onto OS threads (M:N scheduling). When a goroutine blocks on I/O, the runtime parks it and runs another goroutine on that OS thread. Code looks synchronous — no callbacks, no Mono chains.
Spring WebFlux (reactive): A small number of Netty event-loop threads handle all requests using non-blocking I/O. Code must be written as reactive streams (Mono/Flux). High throughput, but a completely different programming model.
Go (net/http / Gin): Each HTTP request spawns a goroutine (~2KB stack). The Go runtime scheduler multiplexes goroutines onto OS threads (M:N scheduling). When a goroutine blocks on I/O, the runtime parks it and runs another goroutine on that OS thread. Code looks synchronous — no callbacks, no Mono chains.
Spring Boot MVC — thread per request
Spring Boot MVC — each request blocks a Tomcat thread
Spring WebFlux — event loop (reactive)
Spring WebFlux — non-blocking, but reactive types everywhere
!Reactive Java is powerful but it's a different programming model — every layer from controller to DB driver must be non-blocking. Mixing blocking JDBC inside a reactive chain stalls the event loop. Teams often migrate to WebFlux only to accidentally block anyway.
Go Gin — goroutine per request
Go Gin — synchronous-looking code, goroutine-based concurrency
Go's M:N scheduler — how goroutines stay cheap
Go uses M:N scheduling: M goroutines are multiplexed onto N OS threads, where N =
When a goroutine blocks on I/O (DB query, HTTP call, file read), the Go runtime parks the goroutine and immediately assigns another runnable goroutine to that OS thread. The OS thread never sits idle.
When the I/O completes, the goroutine becomes runnable again and is scheduled back onto any available OS thread.
Result: thousands of concurrent I/O-bound goroutines, running on just a handful of OS threads — without the code complexity of reactive streams.
GOMAXPROCS (defaults to number of CPU cores).When a goroutine blocks on I/O (DB query, HTTP call, file read), the Go runtime parks the goroutine and immediately assigns another runnable goroutine to that OS thread. The OS thread never sits idle.
When the I/O completes, the goroutine becomes runnable again and is scheduled back onto any available OS thread.
Result: thousands of concurrent I/O-bound goroutines, running on just a handful of OS threads — without the code complexity of reactive streams.
Comparing the models
☕ 1 OS thread per request (Spring MVC)
◎ 1 goroutine per request (~2KB)
☕ Thread pool: ~200 threads default
◎ No pool — goroutines are spawned freely
☕ Thread blocks during I/O
◎ Goroutine parks; OS thread serves others
☕ 201st slow request queues at 200 threads
◎ Millions of concurrent goroutines in prod
☕ Code is synchronous, easy to read
◎ Code is synchronous, easy to read
☕ Thread pool size must be tuned
◎ GOMAXPROCS auto-set to CPU cores
☕ ~(2 × CPU) event loop threads (WebFlux)
◎ N OS threads (GOMAXPROCS = CPU cores)
☕ Must never block event loop threads
◎ Blocking goroutines are fine — runtime handles it
☕ Mono<T> / Flux<T> return types everywhere
◎ Plain return values and error
☕ Entire stack must be non-blocking (R2DBC etc.)
◎ Standard library drivers work as-is
☕ Steep learning curve
◎ No new programming model to learn
☕ High throughput
◎ Equivalent throughput
Why Go does thisSpring WebFlux achieves non-blocking I/O throughput but forces you to rewrite every layer — controllers, services, repositories — in reactive style, using Mono and Flux. One accidental blocking call stalls the event loop. Go achieves the same throughput via goroutines while keeping synchronous-looking code. The runtime transparently parks goroutines during I/O. You get reactive-level efficiency with imperative-level readability. This is the reason many teams migrating from Java find Go's concurrency model refreshing rather than foreign.