Intro
Java and Java Web development Back in 2012
Java web development in 2012 was a very different world from what most teams are used to today. There was no Stream API, so day-to-day collection processing meant explicit loops, helper methods, and plenty of boilerplate. Immutability wasn’t the default mindset either—most code leaned heavily on mutable state. Web applications were typically built around Servlets and deployed to full application servers, and innovation felt slower simply because release cycles were longer.
At the same time, Java had some undeniable strengths. The language was stable and predictable. IDE support was (and still is) best-in-class, which made large codebases much easier to navigate and refactor. Performance was fast and consistent, debugging was generally straightforward compared to more dynamic environments, and the ecosystem was massive. If you needed experienced developers who knew how to build and run serious systems, Java had them.
Because we liked Java quite a lot - and because Web Development using Servlets and Application Servers was painfully slow and annoying, we started Ninja in 2010.
Ninja was among the first Web frameworks that tried to bring a Rails-like sense of productivity and developer experience to Java. Spring Boot did not exist back then. The only other cool (in our opinion) Java Web Framework was Play 1. But we had concerns as it used too much bytecode magic and was about to migrate to Scala.
Ninja still exists today, but it carries many of the paradigms and assumptions from that era: mutability as a default, no framework-level embrace of lambdas, and a fair amount of implicit “magic” to wire controllers and routes cleanly. There’s also custom plumbing in places where modern Java (and modern libraries) would handle things more naturally—like UTF-8 support in properties and messages.
Java in 2026
A lot has changed in Javaland since 2012.
Most notably:
- Java releases now happen far more frequently, and we’re already at Java 25.
stream()and operations likemap()add predictability and a functional flavor to everyday code.- Immutability and
Optionalare part of the mainstream Java toolbox. - We got multiline strings—finally. (Still no template strings yet.)
- Lambdas are dramatically easier to use, including convenient method references.
- Records make it straightforward to create data classes with correct
equals()andhashCode()implementations.
A quest
I’ve wanted to modernize Ninja for a long time—but there was a lot of baggage, and a simple refactor didn’t feel realistic. And since it’s always “useful” to do something slightly stupid when you want to learn, I decided to rewrite Ninja from scratch for modern Java.
Here are my observations on modern Java during my rewrite of Ninja.
Observations on Modern Java
Multiline strings - but no Template Strings
Java didn’t have multiline strings for a very long time. But now we have them:
String myString = """
A long
and funky
String
""";
Finally. What. A. Relief.
Unfortunately - we don’t have template strings yet. Aaahhhhhh.
Something like this simply does not exist. In 2026. Bummer.
String name = "a name";
// The following does not exist as of 2026:
String myTemplate = """
Hi {{name}}!
""";
That’s not nice. I’d expect this as a basic feature of a programming language.
There are lots of discussions around that topic.
But as of Java 25 it’s not in the JDK.
On Functional Immutable Programming
Stream API
The Stream API works surprisingly well. Ok. You always have to call .stream() and often also have a collector. But it’s a joy to use and feels so much better for many operations than a for loop.
Null-safe Programming and Optional
In 2026 most people think that null was a mistake (some don’t).
I personally really think that null is a mistake. Or more precisely - a language that cannot guarantee whether something is null. Many modern languages have something like this:
var person: Person // person is guaranteed by the compiler that it never can become null.
var person: Person? // ...? to signal that it can be null
The language doing this for you is so much better and safer.
But what Java has is Optional. And newer frameworks don’t use null anyway. So either return Person or return Optional<Person>. Works for me and is the way to go for Java in 2026. But again, it’s more error prone than simply forbidding a variable to become null at all at compile time.
On modules
Yea. I don’t use modules. And I have no use case for them. Seems academic.
Switch Case Blocks
At some point Java introduced much improved switch case statements and sealed classes. I know that a lot from Kotlin and Scala, and these are important to make code more readable and more predictable.
You can see a real life example in the new version of Ninja over here. Handling client side sessions is a bit tricky because they are based on the cookie semantics of the browser.
This is where a sealed interface comes in totally handy:
public sealed interface NinjaSessionState permits Exists, Remove, UnknownButDontTouch {
}
public static final class Exists implements NinjaSessionState {
private final NinjaSession session;
public Exists(NinjaSession session) {
this.session = Objects.requireNonNull(session, "session");
}
public NinjaSession getSession() {
return session;
}
}
public static final class Remove implements NinjaSessionState {
// No session field!
}
public static final class UnknownButDontTouch implements NinjaSessionState {
// No session field!
}
… and this then leads to much improved switch logic. Note that you can extract the session (exists.getSession()) when we get a Result.Exists session state.
switch (result.ninjaSessionState()) {
case Result.Exists exists -> {
NinjaSession ninjaSessionForResponse = exists.getSession();
var cookie = ninjaSessionConverter.createCookieWithInformationOfNinjaSession(ninjaSessionForResponse);
httpServletResponse.addCookie(NinjaJettyHelper.convertNinjaCookieToServletCookie(cookie));
}
case Result.Remove remove -> {
var cookie = ninjaSessionConverter.createCookieToRemoveNinjaSession();
httpServletResponse.addCookie(NinjaJettyHelper.convertNinjaCookieToServletCookie(cookie));
}
case Result.UnknownButDontTouch unknown -> {
// Intentionally don't do anything
}
}
Nice!
On records
Records work out of the box. And for simple data classes that’s nice.
My biggest concerns are twofold:
getXYZ Pattern Broken
A record like
public record Person(String firstName, String lastName) {}
will allow you to access the name via
var person = new Person("firstName", "lastName");
person.firstName(); // not getFirstName()
That’s ugly. Java devs know how to access properties: simply hit “get” in your IDE and you get code completion. Ultimately, as a user of an API you don’t know the implementation details (class / record). But that pattern is broken.
Even stranger: if I want to add more functionality like fullName, would I then have to use getFullName() or fullName()?
record Person(String firstName, String lastName) {
public String getFullName() {
return firstName + " " + lastName;
}
}
No Automatic Builder Pattern
The builder pattern is quite useful when you have many properties in your record.
var person = Person.builder().firstName(...).lastName(...).xyz(...).build();
But Java (or records) don’t create that builder for you. So you either have long instantiations like this:
var person = new Person(.. ,.. ,... ,... );
… which of course is hard to read and very error prone.
Or you implement the builder by hand (or via Lombok). I think that’s not ideal.
No Default Values for Classes and Records
In other languages you have default values and also the ability to assign values to parameters of a method:
// ... This would be something I'd imagine in Java
class MyPerson(String firstName, String lastName, Optional<String> middleName = Optional.empty())
// of course the following also does not work in Java 25
new MyPerson(firstName = "a first name", lastName = "")
Default values and assigning values allow you to fix three things at once:
- Fewer errors when creating new classes
- No builder pattern needed
- No need for lots of repetitive default constructors
I’d expected Java to have this in 2026. But to no avail. That’s a bit of a disappointment.
Finding Java Talent with Backend Skills is Simple
An overlooked feature of Java is the very high talent density for backend developers.
That’s often not the case in languages like Python (strong tilt towards AI with less experience building backend systems) and Go (lots of people like to work in the Kubernetes field).
That’s clearly a + for Java here. And still is in 2026.
IDEs Are Still Awesome
Java has been built to be very IDE friendly. And it still shows.
My main IDE is NetBeans. And it is such a joy to use. Debugging is dead simple. Found a bug? Start the debugger. Test not working? Debug it. One click is enough.
Code completion is a joy. And the compile-on-save feature enables Ninja’s superdev mode.
While other languages also have good IDEs and debuggers, my subjective feeling is that Java is just a bit simpler to use, which makes you more productive in your daily work.
Fast Compile Times and Quick Startups
Java has the reputation of being slow. But for my use cases, and for what it provides, this is not the case. The compiler is very fast and snappy.
I am using mostly Maven and NetBeans as IDE. The feedback and compilation is almost immediate on my M1 Mac. There’s zero productivity impact from Java.
Starting a full Ninja application (including database migrations and a full HTTP server) takes around 300ms on a M1 Mac.
That’s a non-optimized startup without any magic. This is fast enough even for pods that get scaled up. One slower request is bad, but 300ms once in a while is acceptable for most users.
Of course you could tune this even further by e.g. class data sharing or GraalVM.
Agentic AI Support
I’ve experimented a lot with Claude Code and OpenCode during the refactoring. Both work very well with Maven + Java codebases. They “get” the mvn test build cycle. And they also “get” refactorings across modules or more complex structures. (Mostly as always with Agentic AI at this point).
Verdict
I enjoyed working with Java for the rewrite. The dev experience is still very nice and the features that were added bring many improvements to the language.
It’s also great to see that the IDEs are amazing, compilation times are not an issue, and that the ecosystem is still thriving.
On the other hand - Kotlin for me still is the “nicer” language when it comes to JVM languages. But the overall package of Java in comparison is not that bad either.
And especially when you have to decide whether to use Kotlin (not as widely used) for a new project or Java (widely used and lots of talent), it is now easier than ever to choose Java. YMMV though. And Golang is cool, too.
