Stop Using @PostConstruct in Your Java Applications

Semyon Kirekov
Level Up Coding
Published in
5 min readJun 8, 2020

--

This story is a sequel to one of my previous articles Stop Using Setters. If you haven’t read it yet, you should check it out.

If you are a Java developer, there is no need to explain the concept of the Spring Framework. We are well aware of @Component, @Autowired, and many other useful annotations. Perhaps you prefer Jakarta EE stack, then your choice is @ManagedBean, @Inject and so on. In both of these cases, there is one thing that seems convenient but makes your code less maintainable and more fragile. The @PostConstruct annotation. In this article, I’ll try to convince you that you should forget about it for good.

Image by Carlos Lincoln from Pixabay

What’s the purpose of @PostConstruct? Usually, we use it to defer some events that must be executed after object instantiation. For instance, suppose our application needs a service that accepts a user’s request and adds it to the queue. How shall we design QueueService? Here is one option.

An example of common @PostConstruct usage

Firstly, QueueService checks whether a user has required permissions. If it’s so, it adds the new request to the NativeQueue. But the queue needs to be initiated in the beginning. We can’t put the initialization inside addQueueEvent, because it should be invoked only once. Using @PostConstruct block seems like a good approach. But if you watch closely, you will notice that it’s not so different from the setter. In both cases, we create something that is not ready for usage yet. And then after some special manipulations, an object becomes complete. Sadly it’s not the only problem. Let’s discuss them one by one.

There is no way to write a proper unit test for QueueService

As you can see, initializeQueue method is private. Spring (as well as Jakarta EE) does not care about it, because the framework uses Reflection API to execute annotated methods. But we do care about it. It’s impossible to cover this method with unit tests due to the fact that it’s inaccessible from the outer scope and nothing calls it inside the class.

How can it be fixed? Well, the easiest way is to change the method scope. We can declare initializeQueue as package-private or even public one. But in the same way, it allows other components to call it whenever they like to. That may lead to unpleasant consequences (like unexpected queue flushing).

One may say that instead, we can use Reflection API to call the method from the test. It means that the functionality still remains hidden to the world. Perhaps I’ll write another story about it, but for now, I just want to say:

Please, don’t use Reflection API in your tests! It will bring you nothing but maintainability hell.

Recently I had a negotiation with my colleague about putting @PostConstruct on private methods. He told me that it’s absolutely fine. And if we want to verify the class, we have to start the Spring Context in our test. I absolutely agree that integration tests should be present as well as unit ones. But I consider that unit tests are necessary in any case. A class must be always verifiable separately from the system. Integration tests should expand but not replace unit tests.

The class becomes more fragile and less reusable

Suppose we need to add some default requests to the queue on system launching. That’s how it can be done.

Queue fulfilling on application startup

The funny thing is that this code is not deterministic. It will do its work but only from time to time. There is no guarantee that @PostConstruct will be executed before QueueFulfillingService instantiation. So, sometimes the app shall be crashed with an unexpected exception.

Can it be fixed? Well, kind of. We can replace @PostConstruct with BeanFactoryPostProcessor and PriorityOrdered interface. The first one defines an action that ought to be executed after the object's instantiation. The second interface tells the Spring the order of the component’s initialization. Although it solves the problem, it shall make our code verbose and too coupled with the Spring ecosystem. Not the best way to deal with such an easy case.

NativeQuery initialization failure means the app crashing

Is the QueueServicean obligatory feature? Not necessary. But if NativeQuery.init() fails, it will crush the whole application. One may say that it’s exactly what we want from the system. If something went wrong, it would be better to identify an error as soon as possible. I doubt this statement. And here is why.

I use Gmail almost every day. If you do as well, you know that you can examine contact info by putting a mouse pointer on the email's sender name. Have you known that this operation invokes another HTTP-request? You can notice it by opening the development console. I think that this feature is not so important. If there are some troubles on the server-side, it’s absolutely fine to see no popup. But if this feature would be verified on application startup? Any error would mean the whole system stop. I think that the price is just too high.

As far as I know, this feature is implemented in a separate microservice. In this case, startup failure would not kill the whole email cluster, but my point is that all needed initializations should be invoked only when it’s needed, not in advance.

Solution

The most obvious solution is to put all the required steps inside a constructor. Although it solves the first two problems (unit testing and reusability), it does nothing with failure on startup. What we need are lazy initializations that must be called only once and when it’s needed. Here is the way.

QueueService with lazy NativeQueue initialization

CachedResultSupplier is a decorator for Supplier interface. It calculates the given lambda only on the first get call. Further invocations return the cached value.

CachedResultSupplier

As a matter of fact, NativeQueue instantiation and initialization executes only on the first call of addQueueEvent. If something breaks, we can notify a user correctly. Because now we are aware of possible errors and we can deal with them in a proper way. Besides, we can add advanced logging and audit to identify an error more efficiently.

Conclusion

I hope that I convinced you that @PostConstruct usage is a bad practice. More than that, it can be easily replaced even without Spring or Jakarta EE features. If you have any questions or suggestions, please, leave your comments down below. Thanks for reading!

--

--

Java Dev and Team Lead. Passionate about clean code, tea, pastila, and smooth jazz/blues. semyon@kirekov.com