Explain the Reactor pattern and how Java NIO Selectors implement it. Why do servers like Netty use it?
answer
- Demultiplex (select) then dispatch to handlers
- Selector.select = the synchronous event demultiplexer
- Never block the event loop; offload slow work
- Multi-reactor: 1 boss + N worker loops (~per core)
- Netty = productionized reactor over NIO selectors
- Reactor = readiness; Proactor = completion (NIO.2)
basics
~20 sThe Reactor pattern uses one (or a few) event-loop threads that wait for I/O readiness and dispatch each ready event to a handler. Java's Selector is exactly this: select() detects readiness, and you route OP_READ/OP_WRITE/OP_ACCEPT to the right code. It lets a server handle many connections with few threads.
solid answer
~50 sThe Reactor pattern decouples waiting for I/O events from handling them. A reactor (event loop) running on one thread blocks on a readiness demultiplexer — in Java, Selector.select() — until one or more channels are ready, then dispatches each event to a registered handler keyed by operation type (accept, read, write, connect). Handlers must be non-blocking and short, because blocking the loop stalls every connection it owns. Because a single thread can watch thousands of channels via epoll/kqueue, you serve massive concurrency with a tiny thread count, avoiding the memory and scheduling cost of thread-per-connection. Real systems use the multi-reactor variant: a small pool of event loops (typically one per CPU core), often with a boss loop accepting connections and worker loops handling reads/writes. Netty, Vert.x, and Node's libuv all build on this; Netty wraps NIO selectors in EventLoopGroups so application code writes handlers, not selection loops.
go deeper
Knows a reactor is one thread that waits for events and calls handlers, enabling many connections with few threads.
Maps the pattern onto NIO (select = demultiplex, SelectionKey dispatch) and can sketch a single-reactor loop.
Explains the never-block-the-loop rule, offloading, the multi-reactor boss/worker design, and why frameworks like Netty exist.
Reasons about reactor sizing per core, back-pressure, connection pinning vs work-stealing, Reactor vs Proactor (NIO.2/io_uring), and trade-offs against virtual-thread blocking architectures.
## The problem the pattern solves Imagine a server with tens of thousands of mostly-idle connections. **Thread-per-connection** (one blocking thread per socket) wastes huge amounts of memory (each thread's stack) and overloads the OS scheduler. We want to **handle many connections with very few threads**, doing work only when a connection actually has something to do. The **Reactor pattern** is the classic design for this. ## The pattern's parts The Reactor pattern (named by Doug Schmidt) has these roles: 1. **Handles** — references to I/O resources (here, channels / their file descriptors). 2. **Synchronous Event Demultiplexer** — a single call that blocks until one or more handles are ready, then reports which. In Java this is **`Selector.select()`** (backed by epoll/kqueue/IOCP in the OS). 3. **Reactor (event loop)** — the thread that calls the demultiplexer in a loop, and for each ready handle dispatches to the right handler. This is your `while(true){ select(); for each selectedKey dispatch; }` loop. 4. **Event Handlers** — the code that does the actual work for an event (accept a connection, read and decode a request, write a response). The key idea: **waiting** for events is separated from **handling** them, and one waiter serves many handles. ## How NIO maps onto it | Reactor role | Java NIO element | |---|---| | Handle | `SelectableChannel` (e.g. `SocketChannel`) | | Demultiplexer | `Selector.select()` | | Reactor / event loop | your selection loop thread | | Dispatch key | `SelectionKey` + `isAcceptable()/isReadable()/isWritable()` | | Handler | the branch you run per ready op (often stored as the key's attachment) | A minimal reactor: register the `ServerSocketChannel` for `OP_ACCEPT`; in the loop, on `isAcceptable()` accept the new `SocketChannel`, set it non-blocking, register it for `OP_READ`; on `isReadable()` read and process; manage write interest on demand. ## The golden rule: never block the loop Because one thread serves many connections, **any blocking or slow work inside a handler stalls every other connection** that loop owns. So handlers must be non-blocking and fast. Long or blocking work (database calls, CPU-heavy parsing, file I/O on a non-selectable `FileChannel`) must be **offloaded to a separate worker thread pool**, with the result handed back to the loop to write out. This discipline is the single biggest source of bugs in hand-written reactors. ## Single vs multi-reactor - **Single reactor:** one loop does accept + read + write. Simple, but one core, so it can't use a multi-core machine and one expensive handler hurts everyone. - **Multi-reactor (the production shape):** a **boss/acceptor** loop handles `OP_ACCEPT` and hands new connections to one of several **worker** loops (commonly *N = number of cores*), each with its own `Selector` and thread. This spreads connections across cores while keeping each connection pinned to a single thread (so per-connection state needs no locking). This is the **main reactor + sub-reactors** variant. ## Why frameworks exist (Netty, Vert.x, libuv) Hand-writing a correct selector loop is error-prone: the `selectedKeys().remove()` trap, the `OP_WRITE` spin, partial reads/writes, the infamous **epoll selector-spin bug** (where `select()` returns 0 in a tight loop), graceful shutdown, buffer pooling, back-pressure. **Netty** packages all of this: `EventLoopGroup`s wrap NIO selectors, `Channel`/`ChannelPipeline`/`ChannelHandler` give you a clean handler chain, and it adds zero-copy buffers (`ByteBuf`), codec support, and back-pressure. You write handlers; Netty runs the reactors. **Vert.x** layers an actor-ish model on Netty; **Node.js** uses the same idea via **libuv**. The takeaway for interviews: NIO Selectors are the *mechanism*; the Reactor pattern is the *architecture*; frameworks are the *productionized implementation*. ## Reactor vs Proactor (bonus) The **Reactor** reacts to *readiness* ("you can now read") and the handler does the I/O. The **Proactor** reacts to *completion* ("the read you asked for is done, here are the bytes") using true async I/O (e.g. Windows IOCP, Linux io_uring). Java NIO selectors are reactor-style; NIO.2's `AsynchronousSocketChannel` (with completion handlers) is proactor-style.
- Why is blocking inside a reactor handler dangerous?The loop thread serves many connections; blocking it stalls all of them. Slow/blocking work must be offloaded to a worker pool and the result written back via the loop.
- How does the multi-reactor variant use multiple cores?A boss loop accepts connections and distributes them to several worker loops, typically one per core, each with its own Selector and thread, so connections are spread across cores while each stays pinned to one thread.
- How does NIO.2's AsynchronousSocketChannel differ from a Selector?It's proactor-style: you submit an operation and get a completion callback when the bytes are already transferred, rather than being told readiness and doing the I/O yourself in the loop.
saying these in an interview costs you the question
- Doing blocking DB/file work inside the event loop thread
- Claiming Reactor needs one thread total (production uses a small pool, often per core)
- Confusing Reactor (readiness) with Proactor/NIO.2 (completion)
- Thinking Netty replaces NIO — it wraps NIO selectors
- Ignoring the selector-spin / OP_WRITE pitfalls a framework handles for you