Perils of Spring Boot and Opinionated Frameworks
Check out this post to learn more about opinionated frameworks and Inversion of Control!
Join the DZone community and get the full member experience.Join For Free
We, developers, like abstraction. Without it, we could not build applications. Our programming disciplines even require that we code to abstractions and avoid coupling our code to detailed implementations.
However, what is the right abstraction for your application?
Sadly, the choice of abstractions really comes from our choice of framework. Frameworks are basically abstract solutions that we extend to solve our problem.
Unfortunately, frameworks like Spring Boot come opinionated about the threading models you use, interfaces you need to extend, possibly the data repositories that are applicable, and various other assumptions about your problem space. That's a lot of restrictions before I've even written my first line of code.
What we really want to do is explore the problem space first. This is what test-driven design is all about. We write tests to define what is successful code. Then, we implement code to pass those tests. As we go along writing tests to cover off requirements, we subsequently churn out working code for the application. In time, we get enough working code to release as the application.
So, this leads me to ask: when do we test the choice of framework?
Opinionated Frameworks Force Abstractions Too Early in the Development Process
Well, I guess we pay very experienced senior people to make this choice. So, this choice must be correct. It would not be for reasons like:
I (or our company) only know this framework, so we are using it
It's new and shiny with lots of buzz words, so we must use it
My CVs a little old, let's try something new
This one is cheaper
Architecture believed what it says on the tin
Regardless of the reason, the only way to test the framework choice is to build the application with it. And just for those of you who like opinionated frameworks (like Spring Boot), please tell me you write the most risky aspects first. This is so you can quickly discover if the framework's opinions match with your problem.
Sadly, even if you test with the most risky aspects, finding out that the framework decision is wrong can lead to a lot of wasted code. This arguably wastes a lot of money for the business and can lead to failing projects.
For example, say we choose Spring Reactive. Yay, we can make concurrent asynchronous calls out to various microservices. We can also use the latest in NoSQL data stores. This was all a great decision. However, over time, we realize that we have a small amount of data where integrity of the data is very important. We find that we want to use a relational database to solve this and then incorporate JPA on this database for easier interaction. However, our choice of Spring Reactive has disallowed this because it requires all I/O to be asynchronous (JPA is synchronous database calls). OK, yes, we can use Schedulers, but I seem to be continually doing workarounds for lack of transactions. The data consistency issues are starting to mount up and we're missing deadlines. I'm now in a position of "do I throw out all the Reactive code, or do I keep making work arounds, hoping it might all hang together? I definitely need to swap jobs before this hits production and we start supporting it. In my next job, I've learned to use Spring Servlets for this type of problem.
The flip side of this could also be easily the case. We start out wanting Spring Servlet for JPA interaction with a database. However, over time, we realize the database interaction is mostly read-only. What we really wanted was asynchronous I/O from Spring Reactive to collect data from multiple microservices and data stores concurrently. Unfortunately, with our upfront Spring Servlet choice, the data collection is just too slow. Our work around is to use async Servlets and spawn threads to make concurrent requests. This worked initially, but over time, the load increased. This significantly increased thread counts, resulting in thread scheduling starvation, which resulted in timeouts. I've really got no way to fix this without significant rewrites of the application. In my next job, I've learned to use Spring Reactive for this type of problem.
So, can we look to test the framework without having to throw out all our code?
Inverting Framework Control
Dependency injection went a long way in inverting control. When I write my Servlet handling method, I no longer need to pass in all my dependent objects. I would define dependencies, via
@Inject , to have the framework make them available. The framework, subsequently, no longer dictates what objects my implementation can depend on.
However, there is a lot more to a framework than just the objects. Frameworks will impose some threading model and require me to extend certain methods. While dependency injection provides references to objects, the framework still has to call the methods on the objects to do anything useful. For example, Spring goes a long way to make the methods flexible, but it still couples you to Reactive or Servlet coding by the required return type from the method.
As I need the Spring framework to undertake dependency injection for my tests, I'm coupled to the particular Spring Servlet/Reactive abstractions before I even write my first line of code — an upfront choice that could be quite costly to change if I get wrong!
What I really want to do is:
Write tests for my implementations (as we are always test driven, of course)
Write my implementations
Wire up my implementations together to become the application
Well, the first two are very simple:
Write tests calling a method passing in mock objects
Write implementation of the method to pass the test
The last becomes very hard. The reason the last becomes very hard is that there is no consistent way to call every method. Methods have different names, different parameters, different exceptions, possibly different threading requirements, and different return types. What we need is some facade over the methods to make them appear the same.
The Inversion of (Coupling) Control (IoC) provides this facade over the method via the
ManagedFunction interface does not indicate what thread to use, what parameters/return types are required, nor what exceptions may be thrown. This is all specified by the contained method implementation. The coupling is inverted so that the implementation specifies what it requires.
This inversion of coupling allows framework decisions to be deferred. As I can have all my methods invoked in a consistent way, I can go ahead and start writing implementations. These implementations may require Reactive coding to undertake asynchronous calls out to different microservices. Some of these implementations may require using JPA to write to relational databases. I really should not care at the start of building the system. I'm tackling the concrete problems to gain a better understanding of the real problem space. I know my methods can be invoked by the framework via wrapping them in a ManagedFunction. We can deal with determining the right framework later on, once we know more.
Actually, this is allowing the implementations to choose the appropriate abstractions to be provided by the framework. My implementations define what objects they require, what other methods they require calling, and what thread models they will require. The implementations are, effectively, defining what abstractions are required from the framework.
Therefore, it is no longer the framework being opinionated. It is your developer code that is allowed to be opinionated.
This allows your implementations to be opinionated about the most appropriate framework to use. No longer do you have to guess the framework based on vague understanding of the problem space. You can see what abstractions your implementations require and make a more informed choice of framework.
In effect, IoC has a deferred choice of the framework to much later in the development process. This is so you can can make the decision much more confidently. And isn't this what Agile says, defer the commitment until the last responsible moment?
In summary, why be forced to make too many upfront decisions about your application? In choosing the framework, you are making some significant choices is solving your problem space. As frameworks are opinionated, they impose a lot of coupling on your solution.
Rather, why can't I just start writing solutions to concrete problems and worry about how they fit together later on? This allows me to make choices regarding the appropriate abstractions (and subsequently, the framework) when I know a lot more about the problem space.
Inversion of (Coupling) Control gives us this ability to defer abstraction and framework choices to much later in the development process, when you are more informed to make the decision correctly.
Published at DZone with permission of Daniel Sagenschneider. See the original article here.
Opinions expressed by DZone contributors are their own.