Bringing Wookiee, Microservice Manager, to the Modern Era
We introduced you to Wookiee in the March 2018 issue of Java Magazine. It’s an application development framework for Scala microservices that works to prevent tedious repeated efforts. Since we last checked in, there has been a major rethink of Wookiee’s core APIs to adhere to and support Functional Programming doctrine—essentially making it the first-in-class framework for functional Scala application development. The hurdles that the Oracle based team encountered during this rethink speak to the challenges many of us have faced in adapting to a newly functional world.
It began not as a formal mandate with a strict timeline but rather it slowly evolved over years of developer interactions. We’d designed this framework, Wookiee, back years before our acquisition by Oracle—and it was starting to look mighty dated. Despite being written in Scala, it was very obviously designed by engineers who cut their teeth on OOP Java principles. Everything was a Trait (Interface), there wasn’t a single Partial Function or closure anywhere to be found, and anyone who was versed in the finer aspects of Scala’s particular advantages found it stifling to use. It was no surprise then when newly onboarded engineers would ponder, “Why not use a more mature and community vetted framework like Spring?” Indeed, years down the road and it was becoming more difficult by the day to justify all the cycles spent to maintain our very own microservices framework—especially given that its tech debt had piled up and the APIs forced users to conform to very non-Scala patterns. At some point it became apparent that explaining away the weaknesses of the library was no longer cutting it, something had to give for Wookiee to survive into posterity.
In looking around at the attendees of the first elaborations it was easy to feel lucky at the team that would be handling this monumental task. We’d all been doing our best to work around the limitations of Wookiee to express the functional methodologies that had become our ideology these past few years—and the enthusiasm to rip up those impediments was palpable. It wasn’t so different from the relief of knowing a problematic library was about to be replaced, but in this case it was literally the fabric of all the dozens of microservices we’ve written. It was apparent to our team that a few things desperately needed to change: Most importantly, in some cases inheritance made sense but overall we needed to allow the use of functions as variables as a replacement for overrides; it’s a fundamental change that is at the base of enabling functional patterns. In practice what this meant is that instead of having a class with overriding implementations all that logic could live in one function that we could pass to Wookiee where it would manage that command and make it accessible anywhere in the service.
As we are a small team it made sense to keep as much as we could, and mainly focus on enabling functional interactions with Wookiee rather than rewriting the entire library functionally. In practice this meant translating Functional back to Object Oriented but letting consumer of the API interact with it in an entirely functional manner. For example, consider these traits we used for adding Commands that already existed in Wookiee:
// Override this to build a Command
trait Command {
def execute[A, B](input: A): B
}
// Extend this in your main Service (App) class to add new Commands to Wookiee
trait CommandHelper {
def createCommand(commandClass: Command): Unit = {
... // Internal Wookiee logic to make this Command accessible anywhere
}
}
Here is how we would have interacted with these traits in the previous, OOP world:
class HelloWorldReturner extends Command {
override def execute[String, String](input: String): String = {
s"Your Text: $input"
}
}
// By extending Service this class wraps the main() method
// and will be the entry point of your service
class HelloWorldOOPService extends Service with CommandHelper {
// Automatically called on Service start
override def addCommands = {
createCommand(new HelloWorldReturner())
}
}
Now instead of changing the Command trait such that it’s entirely functional we were able to cheat a bit to save us time and preserve backward compatibility. Now instead of extending CommandHelper we create a new object which can be used for the same result:
object CommandFactory extends CommandHelper {
// Call this to register a new Command functionally
def createCommand[A, B](functionality: A => B): B = {
createCommand(
new Command {
override def execute[A, B](input: A): B = {
// Call our Command-specific logic with any input
functionality(input)
}
}
)
}
}
With this new CommandFactory we can let users act entirely functionally, as you’ll see in the example below which shows how we would register a Command in this case. The good news on our end is we were able to keep our infrastructure simply by translating, a win-win:
class HelloWorldFunctionalService extends Service {
// Automatically called on Service start
override def addCommands = {
CommandFactory.createCommand[String, String]({ input: String =>
s"Your Text: $input"
})
}
}
It’s as easy as that, note that in the functional Service there is no need for the separate class to hold the ‘execute’ method. All that is necessary is a function, in this case a closure, which can do everything the OOP approach could but allows developers to stick to functional principles.
So, in the end, what our case study proved is that when transitioning to a functional architecture there is a lot of time to be saved, especially considering that the alternative is often a full rewrite. By simply remaking the interaction APIs of your used libraries to allow functional interaction it’s possible to prevent throwing away your hard work of yesterday. Given that at that point your organization is most likely fully committed to functional methodologies it can be taken for granted that all new code will be written using that pattern, leading to an organic shift over time where the OOP legacy pieces are slowly diluted to the point of being in the minority.
Having been in situations where we’ve thrown out entire codebases and taken years to get back to parity, while our competitors developed features all the while, it’s often the best business decision to mix practicality with the imagined ideal. Development is a messy business and it often takes only a matter of hours before one’s perfectly devised program encounters an unexpected snag which forces one to compromise with their ideals of perfection. Being realistic with that fact leads to a bigger picture perspective where it becomes clear that point of programming is to achieve a function– not to be only Functional.