This post shows how NinjaX became a dependency-free 96KB Java web framework core (routing, sessions, rendering). For a CTO, this matters because a smaller, modular core reduces supply-chain/CVE risk, improves startup/deploy speed, and keeps flexibility to add Jetty/modules when needed.
As an interim/fractional CTO, it’s a repeatable playbook to quickly improve inherited stacks: modularize, cut unnecessary dependencies, and make tradeoffs explicit—speeding up delivery, CI/CD, and security reviews.
A quick rewind: Ninja in 2012 was built for a different Java world
When I started Ninja in 2012, Java—and the way we built services—looked pretty different:
- Java 8 wasn’t the baseline yet (and the ecosystem hadn’t internalized the “modern Java” style we take for granted now).
- “Self-contained server in a fat JAR” was still relatively new in the mainstream.
- Fast startup times mattered less than they do in today’s container/serverless-heavy world.
- Lambdas and the overall modern Java ergonomics weren’t part of the day-to-day experience yet.
Ninja was designed for that era: productive, practical, and full-featured.
Why NinjaX: a rewrite with a cleaner, smaller core
Years later I decided to rewrite Ninja into NinjaX.
The goal wasn’t to create a “toy” framework—it was to build something cleaner and leaner:
- A clean core with as few assumptions as possible
- Far fewer dependencies
- Optional add-ons for concerns like:
- templating
- SQL/database access
- JSON
Even then, NinjaX still felt like a complete framework: sessions, solid routing, assets handling—the stuff you don’t want to re-invent for every service.
And personally, it was a great excuse to use modern Java language features—and to lean on AI tooling to accelerate the rewrite and explore what’s realistically possible today.
The verdict at that stage: small, but good.
Joe’s challenge: “What if it had zero dependencies?”
At some point I was chatting with Joe, who co-started Ninja with me and still maintains it together mostly these days.
Joe has always had a very specific vision: a dependency-free web framework.
At the time, NinjaX was already minimal—but not “zero dependency” minimal. The core still relied on a few things most Java web stacks consider non-negotiable:
- logging (SLF4J + Logback)
- a JWT library (for session handling)
- JSON and templating built into the core
- Jetty to actually receive HTTP requests
So “zero dependencies” sounded both ambitious and slightly unreasonable—which made it a great challenge to accept.
Step 1: Modularize NinjaX (make “minimal” a real choice)
The first step was straightforward: modularize.
Instead of shipping a single framework artifact where everyone pays for features they may not use, NinjaX became a set of focused modules, for example:
ninjax-coreninjax-jackson-jsonninjax-db-jdbi- …and other small, dedicated add-ons
This let you build an application by adding only what you actually need.
However, even in this modular world, you still needed a web server module—typically Jetty—to accept HTTP requests.
Outcome (Step 1):
- NinjaX became properly modular.
- The most minimal “fat jar” was around 13MB, basically depending on the JDK plus a small set of libraries.
Minimal enough? Close—but not what Joe had in mind.
Step 2: Replace Jetty with the JDK’s own HttpServer
Java has shipped a built-in HTTP server for a while now:com.sun.net.httpserver.HttpServer
The obvious question was:
If the JDK already contains an HTTP server… can we remove Jetty entirely?
I benchmarked it—expecting “it works, but it’s slow” or “it’s fine for demos.”
To my surprise: it performed really well. Good enough that it became realistic to use it as the default minimal runtime for NinjaX.
Outcome (Step 2):
ninjax-coreuses the JDK’sHttpServer- The minimal fat jar dropped to around 4MB
That was a huge win, because Jetty is great—but it’s also a meaningful dependency in both size and supply-chain surface area.
Step 3: Where do those last megabytes come from?
At ~4MB, the next question was: why is a minimal Java web framework still measured in megabytes?
In my case the remaining size mostly came from two sources:
- JWT library (used for session handling)
- Logging stack (SLF4J + Logback)
So I took the uncomfortable step: replace both.
Replacing JWT
I replaced the external JWT library with an internal implementation suitable for what NinjaX needs.
This is a tradeoff:
- It can reduce supply chain risk (fewer external libraries, fewer surprise CVEs landing in your stack—Log4j is the cautionary tale everyone remembers).
For context: https://www.ncsc.gov.uk/information/log4j-vulnerability-what-everyone-needs-to-know - But “rolling your own” also means:
- creating a minimal Json parser
- more responsibility on us to get the security details right
Replacing SLF4J/Logback
I replaced Logback/SLF4J with JDK logging.
That removes a common dependency chain entirely. It’s also not really a downside as logback has bridges to common logging libraries. So if you want to use logback then go for it!
Then I applied additional shrinking by using the Shade plugin’s minimizeJar feature.
Outcome (Step 3):
- A fully working web framework fat jar of ~100KB (Is 100kb a “fat jar after all?”)
- After jar minimization: 96KB
- Still accepts HTTP requests, supports routing, session handling, and rendering output.
That’s not a “hello world.” That’s an actual framework core.
What you gain—and what you give up
This kind of minimization is fun, but it also surfaces real engineering tradeoffs.
The JDK HttpServer tradeoffs
The built-in server is impressively capable, but it also means:
- Some features you’d get “for free” in Jetty must be implemented and maintained by us
- e.g., multipart/form-data parsing
- That’s more code to own, and parsing edge cases can be both bug-prone and security-sensitive
- There are also limitations—e.g., HTTPS support isn’t simply on-par with full server stacks in the way many people expect (depending on your environment and requirements)
The important part: Jetty is still an option. The minimal core doesn’t remove the ability to run NinjaX the traditional way when you want the mature feature set of a dedicated server.
96KB is the “core minimum,” not your full app
The 96KB result is for the minimal framework core.
Once you add real application features like:
- database access (e.g., JDBI)
- JSON (Jackson)
- templating
- other integrations
…your build grows again. Which is fine—because the point of modularity is that you control what you pay for.
JDK logging in the real world
JDK logging keeps the dependency footprint tiny, but in many organizations:
- Logback is the standard
- teams want a unified logging pipeline
- bridges and adapters bring size back
So again: the minimal setup is there when you want it, but larger systems may intentionally choose a heavier logging stack.
Summary: why “smallest” actually matters
NinjaX ended up with something I didn’t fully expect when this started:
A self-contained Java web framework core—with routing, sessions, and rendering—that fits in 96KB.
That’s not just a fun statistic. The practical benefits are real:
- You can understand the code end-to-end
Small surface area means fewer hidden behaviors and fewer “framework magic” traps. - Very fast startup times
Which matters for modern deployments, CI workflows, and scaling patterns. - Quick, painless cloud deployments
Smaller artifacts are simpler to build, ship, scan, and run.
And if you do need the heavyweight features: you can still add modules (or use Jetty) and scale up.
But now, the minimum is truly minimal.
