It is already 2020 and many companies are still debating what cloud development strategy they should adopt. I am conscious of how many of them still opt for building multi-purpose applications that can run on multiple clouds, while only a few really adopt a native approach for running integrated applications on the cloud platform. And while I can understand that cloud-native development seems like a sky-level risk (am I putting all my eggs in one basket?) I'll try to explain why this is a good (and inevitable) choice.
Writing multi-purpose applications that share code between different platforms is not new. The idea of write once, run everywhere was the mantra during the late 90s but applying it to today's technology seems to be more harmful than beneficial. And I am not talking only about cloud technology. If you don't believe me, look at what happened to Dropbox engineering team with their mobile development strategy
While many companies start implementing their vision of cloud-integrated applications and services, it is an accepted (yet temporary) pattern to deploy these services outside the platform on any on-prem infrastructure. Why? Because they need to keep the business running while they implement this transition. They can't just wait 3 years to have all their systems migrated to the cloud.
By adopting this bimodal strategy, these applications and services are cloud-native at their default implementation, but still portable by applying isolated and well-documented changes to their source code.
Wait, why do we still require changes to our source code for portability and not implement those services with Kubernetes?
Well, because we want our default service implementations to maximize the benefits of the underlying cloud platform. Requirements are usually cost control, managed services, and live deployments (no maintenance windows) and that indirectly pushes you to serverless and cloud-nativeness.
However, it is important that during this transformational period these companies mitigate but not eliminate cloud-nativeness to enable easy service portability outside the cloud platform.
By writing code in a standard way using the platform's defaults, we ensure we benefit from the full capabilities of the chosen cloud. Closer proximity to cloud features makes for simpler implementation. Conversely, biting the bullet to rewrite native code to address a different platform, while on the face of it a pain, is usually fairly straight forward and less costly
Common abstraction approaches invariably address the lowest common denominator, put a brake of innovation, and result in more costly and more complex solutions. That overhead will end up being more expensive than re-writing isolated pieces of code multiple times for multiple platforms.
Wait, re-writing all the apps and services code? Yes. Well, not all of it, only some very isolated parts
During this transitional period, cloud-nativeness can be mitigated but not eliminated. And this is handled at component engineering level
First of all, don't couple your business logic to the platform. Although this seems pretty obvious, it is way more important than it sounds.
Code your business logic to an interface. Again, nothing new, just old school interface-oriented programming. This is, separate functionality into modules that communicate via well known functional interfaces (e.g. interfaces that mean something to the consumer function, rather than being technical abstractions)
Isolate code that accesses cloud services directly via SDK behind such interfaces and use dependency injection to instantiate the appropriate interface implementation at runtime. This is commonly known as The Provider Pattern
- AWS has a service to provide hierarchical configuration information - SSM Parameters
- Design an interface that presents configuration data in a manner that makes sense to the application (a key/value map, perhaps with additional application-specific semantics)
- All consumers of configuration address this interface
- Write a module that implements the interface using the SSM APIs and translates the data into the format demanded by the interface, plus maybe has caching capabilities
- At application start-up, accept an environment variable naming the configuration data implementation to use.
OK, so you want your cloud-native code to run on another cloud? No worries, just reimplement the interfaces for that new cloud platform using the new platform's standards and direct access to services via new SDKs. Well isolated and documented changes, while the core business logic remains untouched.
More importantly, you need to have your application or service running on-prem? Even easier, re-write those interfaces using supported industry APIs (i.e. JMS, MQTT) and de-facto standard providers (i.e. Hibernate for JDBC).
It is clear and recognized that access to platform services via industry APIs or de-facto standards are more portable by nature. However, we can't just wrap every direct access to cloud services with an HTTP interface (API).
Again, by writing code that does not leverage the underlying cloud platform’s defaults, we would take an overhead that we would not have to worry if we stay with these widely used cloud technologies. This overhead will end up being more expensive than re-writing isolated pieces of code multiple times for multiple platforms
However, it is important that every component is exposed through and standard HTTP interface. This is how we will compose cloud-native software.
- Modules are composed by combining different functions
- Components are composed by combining different modules
- Systems are composed by combining different modules
- Solutions are composed by combining different systems
This principle rolls up and down, and it is one of the most important guidelines to build modern software.
Back to our main topic, I think that cloud-nativeness risks are mitigated above the component level.
Cloud-native development is faster, cheaper, simpler, and more productive. In the unlikely event of a change of cloud or platform provider, portability is obtained by identifying very isolated places where a code change is necessary:
- a new interface provider here,
- a new event format transformer there,
- a slightly different container configuration elsewhere
Even easing any necessary on-prem deployments on-prem. Most services provided by clouds are available off-cloud, though usually at a greater overall cost. Just use industry APIs and de-facto standard frameworks.
In short, exploit modularization, interface-oriented programming, dependency injection and you are good to go. Just (age-old) good programming practices!
(Credits to Billy Huy for the cover picture)