The Pros and Cons of Spring SmartLifecycle

Author:  Ken Stevens

When I integrated Kafka with Smile Digital Health earlier this year, I occasionally saw errors on shutdown.  When I googled the error message, a couple of people recommended using Spring SmartLifecycle to fix the issue.  So I thought I’d give it a try.

At first glance, Spring SmartLifecycle looks really neat.  Rather than letting Spring decide the order to call @PostConstruct and @PreDestroy methods on your services, you can enforce a specific startup and shutdown sequence by assigning each service a “phase”.  It looks a lot like the /etc/init.d phases Unix uses at startup and shutdown.

I figured I’d only start with converting those services that actually talk to the outside world.  So I found all my services that open sockets and started converting. I put all the phases in an interface to make them easy to rearrange later.  Here’s what my phases looked like after my first pass:

public interface ICdrSmartLifecyclePhase {
      // POST_CONSTRUCT is here as a marker for where @PostConstruct 
      // fits into the SmartLifecycle.  Beans with negative phases
      // are be started before @PostConstruct are called
int POST_CONSTRUCT = 0;

	int HTTP_SERVER_200 = 200;
	int MLLP_SERVER_210 = 210;
	int HTTP_CLIENT_500 = 500;
	int MESSAGE_BROKER_800 = 800;
	int MESSAGE_BROKER_CONSUMER_900 = 900;
	// This is the service that runs our scheduled cleanup jobs
int SCHEDULER_1000 = 1000;
}

After going through the process of converting the services to SmartLifecycle, I have to say I was not happy with the result and in discussion with the team decided to back the change out.  

Here are my findings:

Pros

  1. Complete control over start() and stop() execution order.  This can be particularly important if one component depends on the other but Spring doesn’t know about the dependency (more on this later).
  2. The promise of more robust integration tests.  Often times with integration tests when you’re mocking out a part of the system, you can end up in a situation where a subcomponent isn’t ready when your test needs it and you have to jump through hoops to work around this.  Explicit startup phases could simplify this problem.
  3. It was the recommended solution to a specific Kafka shutdown error I experienced.  (Because Kafka is so decoupled to begin with, it’s easy for Kafka to have a decoupled dependency that Spring doesn’t know about.)
  4. You give more prominence in your code to services that interact with the outside world.  There is an architectural benefit to having some of your services stand out as SmartLifecycle services–they’re the big boxes in the architecture diagram. 

That being said, there were a disappointing number of Cons to working with SmartLifecycle that ultimately led us to back it out.

Cons

  1. It’s intrusive:
    • start() method can’t throw exceptions whereas @PostConstruct can, so you have to write a bunch of boilerplate to deal with checked exceptions in your start() methods.  This is especially a pain if your checked exceptions have unit tests.
    • isRunning() method must be implemented.  If you want to do this properly, you need to add a flag to your service and set it in start() and unset it in stop().  And who calls isRunning() anyway? Does Spring ever call it? What happens if it lies? Not documented anywhere.
  2. @Lazy directives are ignored.  SmartLifecycle starts your bean even if it’s marked @Lazy.  This is not what anyone would ever want. You can exclude your @Lazy services from SmartLifecycle, but how do you keep future maintainers from accidentally doing this?  I want patterns in my architecture that enforce proper design, not traps lying in wait to snare unsuspecting developers.
  3. Limited adoption.  There are very few posts about SmartLifecycle which leads me to believe it’s not widely used.
  4. Limited documentation.  The Javadoc is weak. Even the Spring documentation on it is unusually light.  And there aren’t any decent guides on it out there. I couldn’t find the answer to my isRunning() question anywhere.
  5. Most significantly, is it solving a problem we really even have?  In almost all cases where one service depends on another, we already have an explicit @Bean dependency (usually expressed through an @Autowired annotation).  In the few rare cases where that is not the case (e.g. with Kafka), we can just use a @DependsOn annotation to make Spring aware of the dependency. Because if Spring is aware of a dependency between two beans, then Spring already calls @PostConstruct and @PreDestroy in the expected order.

So in conclusion, rather than dealing with the intrusiveness of SmartLifecycle, you’ll be further ahead if you just add @DependsOn to the few unexpressed dependencies where you need to enforce startup and shutdown order.