Over the years, we — the Java/JVM community — have developed a fear of writing public static void main(...)
by hand. We either managed to get rid of it completely by using application servers, or limited it to a crippled form when using Dependency Injection frameworks like Guice
or Spring
. Is it the right way to go?
On the contrary. The main()
method — following the dictionary definition
— is, or rather should be, the “chief in size, extent, or importance; principal; leading” method of our program (well, maybe not in size! :) ). If it’s so important, it should have a prominent place in our codebase! Not the stub that we often have (if at all), but a proper, carefully designed startup sequence of the system that we are writing.
Application servers, DI containers and annotations did help in advancing our overall approach to writing software. But, it’s time to move on. Our languages have evolved. We are no longer constrained by Java 1.5. We now have lambdas in Java, there’s Scala , Kotlin , Ceylon and a host of other languages. We’re coming to appreciate all the benefits of Functional Programming and learning how to best blend it with our current development practices.
The main()
method isn’t only the main entry point for the runtime when executing our program. It’s also the main entry point for reading the code (and as we all know, making the code easy to read is more important than easy to write). It’s the best place to start when we’re wondering what does a program do. Does it expose any http endpoints? Does it connect to a database? Does it register in a service registry? In what order? These questions can be answered quickly and clearly with a well-written main()
.
Event listeners?
Listening for events in the wild
Events and event listeners — for example an application-startup event — often take the place of the main()
method, but only to some extent. Usually, if we want to do some initialisation work, we would use an event listener. But while they are really useful, and a very good decoupling tool, event listeners are no replacement for an explicit, clear startup sequence. And expressing a sequence of steps that needs to be followed is one of the fundamental constructs when programming, so there’s no reason not to use it.
One thing that events are particularly bad at is maintaining proper order. There are work-arounds — such as specifying the order in which event listeners are fired — but it’s definitely better not to have to resort to work-arounds in the first place!
For example, there’s a significant difference if we first try to bind to a port and then register in a service registry, or the other way round. If for whatever reason the bind fails, we might end up with a non-functional service being registered in the registry or — if the startup sequence is properly coded — avoid such situations. Another good example is priming caches. Very often, before a service can start serving requests, the caches need to be refreshed for the first time, and only then http requests can be served.
A good use-case for an event listener is plugging into the lifecycle of a third-party component or library which we are using; but we shouldn’t treat our application as a third-party component.
Startup is important
Exposing http endpoints, connecting to a database, priming caches — these are all examples of essential procedures required in a system. How components are initialised, in what order, and how errors are handled are very important aspects of the system’s inner workings. And the main()
method is a very good place to make these explicit.
The startup procedure might be more important than you think. Why hide it?
Road to recovery
DIY wiring
While we cure our fear of main, it might be a good occasion to stop fearing new
as well. It’s a similar story: we have managed to almost eliminate the usage of new
by using DI frameworks, which do all the object-graph wiring for us, sometimes guided by a set of helpful annotations
. It might seem convenient, and it is at first. However, by giving up control over the way the object graph is created, we are giving up a lot of freedom. We trade quick bootstrap of a new project for certainty, control and explorability of the codebase after it grows and ages.
And there’s no better place to create your object graph than the main()
method! It’s quite flexible as well — we can create singletons, factories, dynamically choose implementations basing on configuration etc. just by using the host language. Java, Scala, Kotlin are all quite expressive languages. While it might seem less fancy at first, regaining full control over the startup sequence and object graph creation is in fact quite liberating. Try it!
Of course, all of the best-practices that we follow when writing “regular” code apply to the main()
method as well; we shouldn’t allow it to become bloated and unreadable, splitting it into methods & classes or introducing abstractions. It might involve multiple methods and classes: but the difference here is that there’s still one clearly defined entry point to our system, with a clear startup sequence. If we need to know the details of a particular step — go-to-definition in your IDE is there for you to explore.
Summing up
Let’s return main()
and new
their proper roles in the code that we write every day!