Why I Built a Java MVC Framework From Scratch — And What I Learned
How building Shock — a lightweight Java MVC framework from scratch — taught me more about Java web development than any tutorial or course ever could.
Why I Built a Java MVC Framework From Scratch — And What I Learned
How building Shock taught me more about Java web development than any tutorial or course ever could.
Everyone tells you: "Don't reinvent the wheel." It's good advice — mostly. Production systems should use battle-tested frameworks like Spring Boot, Quarkus, or Micronaut. But there's a difference between using a tool and understanding it. I wanted to understand.
So I built Shock — a lightweight Java MVC framework with zero required external dependencies. No servlet container. No Spring. No Netty. Just raw ServerSocket, a hand-written template engine, and an Active Record ORM. All in a single repository.
This is the story of why I built it, the architecture decisions I made, and the lessons I learned along the way.
The Spark
I was deep into Jakarta EE and Spring Boot. I could follow tutorials, copy annotations, and get things working. But I didn't understand why they worked.
I knew @Autowired injected dependencies, but I didn't understand the container lifecycle. I knew @RequestMapping mapped URLs, but I didn't know how the dispatcher servlet actually resolved a request. I knew @Entity mapped to a database table, but I didn't know what happened between the ORM and the SQL.
So I did what any reasonable developer would do: I decided to build my own framework from scratch. I called it Shock — because that's what my CPU felt like when I realized how much work HTTP parsing actually is.
What Shock Does
Shock is a from-scratch Java MVC framework that provides:
- HTTP Server — Raw
ServerSocketwith multi-threaded request handling. Manual parsing of request line, headers, and body. - Routing — Register
GET,POST,PUT,PATCH,DELETEroutes with method references. URI path parameters (:id) and query parameter parsing. - Controllers — Extend a
Controllerbase class, define static handler methods acceptingRequestandResponse. - Middleware — Global and named middleware pipelines. Apply middleware by name to specific routes.
- Template Engine — Hand-written lexer and recursive descent parser supporting
{{variable}},{% if %},{% for %}, and layout-based rendering. - Database ORM —
DBConnectionwith built-in connection pool,Builder,Repository,Mapper,EntityManager,RelationManager. Annotations:@Table,@Column,@Primary,@BelongsTo,@HasMany. - Migrations —
MigrationGenerator(dialect-aware CREATE TABLE SQL) andMigrationRunner(versioned migration files). - Session —
SessionManagerwith session middleware. - Multi-dialect — Supports MySQL, PostgreSQL, and SQLite through a
Dialectinterface.
The entire thing compiles with Java 24 and Maven. Zero required external dependencies — the JDBC driver is activated via Maven profile.
Architecture Deep Dive
The HTTP Server
The foundation is a raw ServerSocket listening on a configurable port. Each incoming connection is handled in a separate thread. The request line, headers, and body are parsed manually — no HttpServletRequest, no servlet API.
This was the most eye-opening part. When you parse HTTP by hand, you learn exactly what a request looks like on the wire. You learn why headers matter, how chunked transfer encoding works, and why Content-Length is not optional.
// Simplified: Server core loop
ServerSocket serverSocket = new ServerSocket(port);
while (running) {
Socket clientSocket = serverSocket.accept();
threadPool.submit(() -> handleRequest(clientSocket));
}
The Dispatcher
The ControllerInvoker uses reflection to scan for controller methods at startup, building a route map: Map<RouteKey, RouteHandler>. On each request, the router matches the HTTP method + URI path to a handler, resolves parameters from path variables, query params, and request body, then invokes the method.
RouteKey key = new RouteKey(req.getMethod(), req.getPathInfo());
RouteHandler handler = routeMap.get(key);
if (handler == null) {
resp.setStatus(404);
return;
}
Object result = handler.invoke(req, resp);
The Template Engine
This was the most fun part to build. I wrote a regex-based lexer that tokenizes template source into Token objects (Text, Variable, If, For), then a recursive descent parser that produces an AST. The AST is evaluated against a context to produce the final HTML.
It supports:
{{variable}}— Output context variables{% if condition %} ... {% elseif %} ... {% else %} ... {% endif %}— Conditionals{% for var in list %} ... {% empty %} ... {% endfor %}— Loops with empty-state handling- Layout-based rendering — Views inject into a base template via
{{content}}
The ORM
The database layer follows an Active Record pattern. DBConnection manages a connection pool. Builder constructs SQL queries fluently. Mapper converts ResultSet rows to Java objects via reflection. EntityManager handles CRUD operations. RelationManager handles eager loading of @BelongsTo and @HasMany relationships.
Dialect-aware migrations mean the same entity definition generates different SQL for MySQL, PostgreSQL, or SQLite:
public interface Dialect {
String createTableSQL(TableDefinition def);
String columnType(Class<?> type);
String autoIncrement();
}
Lessons Learned
1. Reflection is Powerful but Dangerous
Java's reflection API is the backbone of any DI container and ORM. It lets you inspect classes, invoke methods, and access fields at runtime. But it's also slow, breaks on module boundaries (Java 9+), and makes debugging genuinely painful. Every NullPointerException in a reflection call stack is a small tragedy.
2. Convention Over Configuration is a Tradeoff
Spring's auto-scanning is magical — until it isn't. When a bean isn't injected and you spend 20 minutes realizing it's because your class is in a package the component scan doesn't cover, you start to appreciate explicit configuration. Shock uses explicit route registration. It's more verbose, but the behavior is predictable.
3. The Servlet API is Ancient but Solid
HttpServletRequest and HttpResponse haven't fundamentally changed in over 20 years. They're verbose, they're ugly, and they work. Understanding them at a low level — by building without them — makes every higher-level framework make more sense.
4. HTTP Parsing is Harder Than You Think
Content-Type boundaries, chunked encoding, keep-alive connections, header injection — HTTP has sharp edges everywhere. Building a parser from scratch gives you a deep appreciation for the libraries you normally take for granted.
5. Testing a Web Framework Requires an Ecosystem
You can't unit test a web framework with just unit tests. You need an embedded server, HTTP clients, response parsers, and database fixtures. I used Jetty for integration testing — which was an educational experience in itself.
6. Zero Dependencies is a Constraint, Not a Goal
Building without external dependencies forces you to understand every line of code in your project. But it also means you're reinventing connection pooling, thread management, and MIME type mapping. For a learning project, this is ideal. For production, it's irresponsible.
Should You Build One?
Yes — if your goal is to learn. Not for production use. Use Spring, Quarkus, or Micronaut for that.
But building a framework teaches you:
- How dependency injection actually works under the hood
- How HTTP routing and dispatching work at the protocol level
- How ORMs bridge the gap between objects and relational data
- How to design APIs that other developers will use
- How to balance flexibility with simplicity
- How much work "magic" annotations actually do for you
Shock is on my GitHub. It's not production-ready, and it never will be. But it's the project that taught me more about Java web development than any tutorial, course, or certification ever did.
Sometimes the best way to understand a wheel is to build one yourself — even if it's square.
If you've built your own framework or have thoughts on learning through reinvention, I'd love to hear from you. Find me on GitHub or drop a message through my portfolio.

Whilmar Bitoco
Full-Stack Developer & Aspiring Cloud Engineer