For the Love of Modern Java
Magnolia in action
Take 12 minutes and a coffee break to discover how Magnolia can elevate your digital experience.
This article isn’t about the latest version of Java; it is about the evolution of the types of problems we can solve with it: using external services, dealing with synchronous calls, and handling many different dependencies that we don’t control.
When I learned Java, I learned about objects and inheritance, interfaces and implementations, exceptions, synchronous code, debugging, dependency injection, and control of null. Chances are, you learned Java in a similar way.
If you are lucky and younger than me, maybe someone also explained to you some of the new features that were introduced since Java 8, like lambda expressions, streams, or modules. But even if you are young, I’m pretty sure that many of you throw exceptions and do null checks. So, you are not programming in a functional way (yet).
We have been used to coding this way for years and haven’t thought much about doing it differently. Maybe you are using some Java 8 features, but in the end, you are still using the usual Java patterns. In this blog, I would like to encourage you to challenge your way of programming.
Inheritance
Java is an object-oriented language, so two of the first things you learn about are objects and inheritance. For example, you need to create an object to write a simple “hello world” program.
But inheritance is not mandatory nor free: you are creating a dependency between your classes. So, think twice before extending a class and consider composition instead.
Composition is the concept of using code snippets from other classes, without creating a dependency between them. One example of composition is dependency injection.
However, I prefer to use static functions or default interfaces introduced in Java 8. Be careful with static context though and try to avoid any kind of state. It is better to pass all objects to the function you are calling.
There are many articles that explain the benefits of using composition over inheritance in detail, but in summary, I can say that your code will be more reusable if you avoid inheritance.
To make the shift from inheritance to composition, ask yourself if inheritance is the best way to solve a problem, for example when developing a UI. You don’t have to be radical as Java allows inheritance and composition to coexist.
Functional programming
If you are reading this and want to modernize your Java code, I recommend you start coding in a functional way. When using Java you can develop in an object-oriented and functional way at the same time. Clever programming takes the best from both worlds.
Functional programming is all about statelessness and passing data between functions: immutability everywhere. Objects and data can’t change. Functions can be arguments in methods, for example using lambdas. Exceptions are not allowed and null doesn’t exist.
However, using lambdas, Stream, and Optional alone doesn’t mean that you are developing in a functional way.
To learn how to develop in a purely functional way, you can explore functional languages like Haskell, Elixir, Frege, and Clojure in the JVM. There are also libraries in Java that offer functional features as Vavr.
I admit functional programming can be hard with a chain of function calls of map, flatMap, and reduce. But don’t be afraid of this change. It will open your mind and your code will be better in many ways: more reusable, maintainable, readable, and with improved error handling.
Creating your own @FunctionalInterface and returning Optional instead of throwing exceptions is a good way to get started with functional programming.
External code
In a world of microservices, REST APIs, SaaS, and AWS, we have to deal with code that we don’t control.
Sometimes we get a Java library, other times we get an HTTP client to interact with these services. In all cases, we have to create an interface and I recommend you follow the best practices below:
Avoid exposing an object or library that is not part of your codebase directly to prevent problems in case the external service changes. Offering your own classes and interface allows you to update your implementation rather than changing your interface when the external service changes.
Your methods shouldn’t throw exceptions because if they did, you would have to check the exception every time you use the interface. As of Java 8, you can use Optional.
Finally, all methods should have a return value, and void isn’t allowed. When doing an asynchronous call, it is good practice to return CompletableFuture.
Let me show you some code to summarize it:
public interface MyExternalService {
//Bad method examples
void save(Map object);
any.external.Object getDetails(String something);
Map savePoint(int x, int y) throws java.lang.IOException;
//Good method examples, Car and Details are classes that we control
Optional<Details> getDetails();
CompletableFuture<Car> save(Map car);
}
Managing hundreds of code branches with Jenkins
Maintaining hundreds of Java modules with active releases can be a challenge. Read our blog to learn how a modular approach based on Jenkins pipelines can help you build, test and deploy artifacts flexibly.
Error handling
If you implemented the changes I proposed so far, you are on a good track and will suffer a lot less pain from error handling, which typically results from these 4 situations:
Unhandled exceptions
Methods you called without checking their return value
Objects that are not in the state you expect
Null checks
To avoid these, remove exceptions and return Optional results. When Optional is not sufficient because you need more information about the error, use an Either class, a class with two properties: the result and the error. There are no Either or Pair classes in Java but you can create your own or use one of the functional libraries for Java.
I also recommend validating the input data of a function before calling the method that processes it. If you separate these responsibilities, you can return the error before the method is called. If you receive external calls, make sure they cannot break your code if some evil sends something ugly.
If an unexpected error does happen, let the app crash. Don’t try to hide possible exceptions with generic try blocks with Throwable or Exception in the catch. Unexpected exceptions should be “exceptional”, not something that is hidden but happens all the time.
At this point, you might have to undo a few things to return your system to a healthy state.
And if your error handling is going crazy, consider reducing your code’s complexity using events to handle errors.
Modules
In Java 9, a new module system was introduced but in our microservices universe, we risk losing the concept of modules.
Microservices are very useful but can be hard to coordinate well and introduce complexity unnecessarily. So, unless it is a requirement, don’t create tons of microservices. Instead, create small, independent, well-tested modules with good error handling that are easy to use, easy to remove, and free of framework dependencies. If you need to provide a microservice later, just create an interface for your module.
This way, you won’t repeat code across your services, your colleagues will use your code, and you will be able to test your system easier.
It is good to hide some of the classes improving the encapsulation. Encapsulation is good for you, but not so good for users of your code because they cannot reuse or override those classes.
Java 9 modules are an optional technology to add to your stack. If you are not comfortable using them, feel free to use normal jars.
Java may be old but ...
Maybe Java isn’t cool because it’s old, but Java is compatible and has many tools, a huge community, and an amazing ecosystem. While its evolution is a bit slow, it is definitely evolving: Java is functional and container-ready, it rides the microservices wave, it has new releases every six months, and with project Loom, many amazing features are coming to the JDK.
Don’t get me wrong. I like other languages, too, but I prefer to evolve my development skills in modern Java. I also try to introduce my colleagues to new Java development patterns that will improve our job tremendously.
So, if you feel that you are using an old language and are thinking about a change, consider changing the way you code and embrace new ideas and patterns that Java offers, rather than writing in another language.