This Mini Tomcat is implemented in roughly 300 lines of pure Java. Its core capabilities include port listening, HTTP request parsing, Servlet route dispatching, and static file responses. It addresses a common pain point for developers: knowing how to use Tomcat without understanding how a web container actually works. Keywords: Mini Tomcat, HTTP server, Servlet.
The technical specification snapshot captures the project at a glance.
| Parameter | Description |
|---|---|
| Language | Java |
| Protocol | HTTP/1.1 |
| Code size | About 300 lines |
| Concurrency model | ServerSocket + ExecutorService thread pool |
| Routing mechanism | Map-based Servlet path mapping |
| Static assets | Read directly from the file system |
| Core dependencies | JDK standard library |
| Startup port | 8080 by default |
This Mini Tomcat reproduces the smallest complete web container loop.
The goal of the project is not to replace Tomcat. Instead, it compresses the most critical execution path of a web container into something small enough to read in one sitting. You can directly observe how a request enters through a socket, gets parsed, dispatched, executed by business logic, and finally assembled into a valid HTTP response.
It covers four foundational capabilities: listening on a port, parsing the request line and headers, routing by URL to a Servlet, and returning either static files or an error page. This makes it highly valuable for understanding the underlying network model before moving on to Tomcat or Netty.
The request lifecycle can be modeled as a unified dispatch pipeline.
After a client connects to ServerSocket, the server reads the first request line to extract the method and path. It then decides whether the target is the home page, a dynamic Servlet, a static resource, or a 404 response. This is effectively the smallest possible version of a container dispatcher.
public void start() throws IOException {
serverSocket = new ServerSocket(port); // Listen on the port
running = true;
registerDefaultServlets(); // Register default Servlets
while (running) {
Socket client = serverSocket.accept(); // Accept a client connection
threadPool.submit(() -> handleClient(client)); // Handle requests concurrently with the thread pool
}
}
This code performs the three core actions of container startup, connection acceptance, and concurrent dispatch.
The server engine handles connection management and routing decisions.
SimpleTomcat is the brain of the entire project. It manages the port, working directory, Servlet mapping table, and thread pool, and it completes request dispatching inside handleClient(). Compared with real Tomcat, this version removes the complex container hierarchy and keeps only the most essential responsibilities.
It also distinguishes between dynamic and static requests: the root path returns a welcome page, a matching mapping executes a Servlet, and all other paths attempt to load a file from disk. If the file does not exist, the server returns 404. This design already resembles the core idea behind a real web container.
Route dispatching depends on path mapping rather than complex configuration.
private void handleClient(Socket client) {
try (client;
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
OutputStream out = client.getOutputStream()) {
String requestLine = in.readLine(); // Read the request line
if (requestLine == null) return;
String[] parts = requestLine.split(" "); // Split method, path, and protocol by spaces
String method = parts[0];
String path = parts[1];
SimpleRequest request = new SimpleRequest(method, path, in);
SimpleResponse response = new SimpleResponse(out);
if (path.equals("/")) {
serveWelcomePage(response); // Home page
} else if (servletMapping.containsKey(path)) {
servletMapping.get(path).service(request, response); // Dynamic handling
} else {
serveStaticFile(path, response); // Static file handling
}
} catch (Exception e) {
e.printStackTrace();
}
}
This logic defines the container’s core pipeline: accept, parse, dispatch, and respond.
The request parser converts raw text into an operable object.
SimpleRequest turns the HTTP text protocol into a Java object. It parses three categories of data: the request method, request path, and request headers. If the URL includes a query string, it also extracts parameters and stores them in the params map.
This encapsulation allows upper-layer Servlets to avoid reading directly from the socket input stream. Instead, they can call getMethod(), getPath(), and getParameter() just like they would with the standard Servlet API. That is the value of abstraction.
Query parameter parsing is a key step for application usability.
private void parseQueryString(String query) {
for (String pair : query.split("&")) {
String[] kv = pair.split("=", 2); // Split a key-value pair
if (kv.length == 2) {
params.put(kv[0], kv[1]); // Store the request parameter
}
}
}
This code converts name=Bob&age=25 into a parameter map that application code can read directly.
The response object builds a valid HTTP message.
SimpleResponse encapsulates the status code, response headers, output stream, and character writer. The key detail is delayed header transmission: the server does not actually write the status line and headers until the first call to getWriter() or getOutputStream().
This design lets business logic set Content-Type, Content-Length, and the status code before writing the body, which avoids the problem of trying to modify headers after they have already been sent. That behavior matches how real Servlet containers work.
HTTP responses must strictly follow the message format.
private void sendHeaders() throws IOException {
if (headersSent) return; // Prevent duplicate header transmission
headersSent = true;
StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1 ").append(status).append(" ").append(statusText).append("\r\n");
for (Map.Entry<String, String> entry : headers.entrySet()) {
sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n");
}
sb.append("\r\n"); // Blank line between headers and body
output.write(sb.toString().getBytes("ISO-8859-1"));
}
This code assembles the status line, response headers, and blank-line separator in the standard HTTP format.
The Servlet interface decouples business logic from the server kernel.
SimpleServlet contains only one method, service(), but that is already enough to express the container’s extension point. The server handles dispatching and does not care about page-specific logic; business classes focus only on reading the request and writing the response.
This is the classic strategy pattern: different paths bind to different implementations, and the container invokes them by mapping. If you later want to add filters, interceptors, or annotation scanning, you can continue building on top of this abstraction.
@FunctionalInterface
public interface SimpleServlet {
void service(SimpleRequest request, SimpleResponse response) throws IOException;
}
This interface defines the minimum contract for dynamic request handling.
The example application demonstrates the full dynamic page generation flow.
HelloServlet reads the name parameter from the request, sets the HTML response header, and writes both a greeting and the current time to the page. In essence, it acts as a minimal controller while also assembling the view.
This example makes the separation of responsibilities easy to see: the container provides infrastructure, while the Servlet generates business output. That clear division helps explain how MVC maps onto the runtime internals.
@Override
public void service(SimpleRequest req, SimpleResponse res) throws IOException {
String name = req.getParameter("name"); // Read the request parameter
if (name == null || name.trim().isEmpty()) {
name = "Friend"; // Set a default value
}
res.setContentType("text/html; charset=utf-8"); // Set the response content type
PrintWriter writer = res.getWriter();
writer.println("
<div>👋 Hello, " + name + "!</div>"); // Write dynamic content
}
This code shows how the smallest possible dynamic page rendering works.
Static resource handling makes the server practically usable.
In addition to dynamic routing, this project can read HTML, CSS, JavaScript, images, and other files from the working directory, then set the Content-Type according to the file extension. That step turns Mini Tomcat from demonstration code into a tiny HTTP server with basic real-world usability.
The project also implements a welcome page, special handling for favicon, and a 404 page. Together, these details form a complete, runnable sample of a minimal container.
This project reveals the evolutionary direction of real Tomcat.
TinyTomcat already contains the prototype skeleton of Tomcat, but real Tomcat improves it by orders of magnitude in concurrency, container hierarchy, lifecycle management, session tracking, filter chains, security domains, and multi-protocol support.
That is why this project works best as a microscope for fundamentals. First, understand the essence of a container. Then return to the real Tomcat source code. Complex structures such as Connector, Container, Context, and Wrapper become much easier to understand once the core execution path is clear.
FAQ provides structured answers to common questions.
1. What is the core difference between this Mini Tomcat and real Tomcat?
Real Tomcat includes a full container hierarchy, NIO/AIO connectors, Filter/Valve extension chains, session management, security authentication, and lifecycle control. Mini Tomcat keeps only the main request-processing path, which makes it suitable for learning principles but not for production use.
2. Why use a thread pool to handle sockets instead of a single-threaded loop?
A single-threaded model blocks all other connections when one request stalls. A thread pool can process multiple client requests concurrently, which is the prerequisite for basic server throughput and is also closer to how traditional Tomcat handles request execution.
3. What capability is most worth adding next?
The highest-priority improvements are POST body parsing, a more complete status code system, URL decoding, static resource caching, and a Filter mechanism. Those features would upgrade the project from a teaching sample to a more complete lightweight web container.

AI Visual Insight: This image is an animated prompt that encourages readers to share the article on WeChat. It does not contain technical information about the project architecture, code flow, or runtime interface, so its value for technical visual analysis is limited.
[AI Readability Summary]
This article breaks down the core request lifecycle of a minimal runnable web container through a 300-line Java implementation of TinyTomcat: port listening, HTTP request parsing, Servlet route dispatching, static resource delivery, and response construction. It helps developers quickly understand how Tomcat works under the hood.