Intro
Think about the capabilities that our application depends on to accomplish specific functions, like:
- File Storage
- Authentication Provider
- AI Provider
We need file storage to save uploaded files. We need authentication provider to implement secure login system, and we need AI provider, if we want to integrate AI functionalities.
But each one of the capabilities, has options to choose from.
For example:
- File Storage (Amazon S3? MinIO? Or even Local storage?)
- Authentication Provider (Okta? Microsoft Entra ID or even self-serve?)
- AI Provider (OpenAI, Anthropic, Grok)
Wouldn’t it be nice, if we could switch between those options with simple configuration setting while developing?
Imagine that Amazon S3 is hard-coded into our Business Logic. If we want to change the storage vendor to some other vendor, we will need to fix the whole entire business logic to handle the newly selected vendor.
That’s a hassle. Not easy to maintain.
So just as we separated all the functionalities within an app into components(frontend, backend, worker) for maintainability, we also separate Business Logic and capability, so we can better easily maintain our app.
If we want to change the storage vendor, no problem, just fix the config file and the Factory Pattern will create appropriate implementation, return that instance and send it back to Business Logic. No need for Business Logic editing, because that’s the whole point of separating the two.
In Cloud Native architecture, this capability is represented by Abstraction. Abstraction represents a capability that Business Logic depends on to accomplish specific tasks. We need many capabilities like AI, Storage, Authentication, Queues etc, and they’re all Abstractions.
Implementation = Actual Service That Will Be Used For Abstraction
Common Abstractions in an app include:
- Storage
- AI Provider
- Authentication
- Queue
And each Abstraction, has implementation options to choose from, for example:
- Storage(Amazon S3, MinIO, LocalStorage)
- AI Provider(OpenAI, Anthropic, Grok)
- Authentication(Okta, Microsoft Entra ID)
- Queue(Redis etc.)
Two keywords to remember so far. Abstraction and Implementation.
Abstraction is a capability. An Implementation is the concrete service that fulfills an Abstraction.
How Abstraction Works
Now we know about Abstraction and Implementation, let’s go over the workflow how Business Logic receives Implementation so it can work with it.
Business Logic
(I need Storage to save files!)
↓
Abstraction
(Storage capability is available!)
↓
Interface
(Implementation will support these actions: save, delete, open)
↓
Read Configuration
(Read config file and find out which Implementation(“Amazon S3” “MinIO” or “LocalStorage”) is set for the project.)
↓
Factory Pattern
(Creates implementation instance according to the config setting.)
↓
Dependency Injection
(Inject implementation instance created in Factory Pattern to Business Logic)
↓
Business Logic
(Implementation received and can work with it)
Let me explain each step in more detail below.
Business Logic
For example, Business Logic wants to save an uploaded PDF to a Storage service. So the business logic states that it needs to depend on the Storage abstraction, to accomplish the save action. So basically Business Logic will say out loud: I need Storage!!
Another piece of business logic might need Authentication, or AI or any other capability that its required to accomplish a task. (All of them, Abstraction)
Abstraction
A capability Business Logic depends on. (Storage, Authentication, AI etc.)
Interface
A new term here. Interface. An Interface defines the operations that every implementation of Abstraction must support.
Whether the implementation is Amazon S3, MinIO, or LocalStorage, they must all provide the same operations like save, delete, and open.
Implementation
An actual service that fulfills the interface contract.
For example for Storage abstraction have implementation choices to choose from:
- Amazon S3
- MinIO
- LocalStorage
And each implementation provides the same Interface(save, delete, open).
Factory Pattern
The application reads the configuration file to find out which implementation is set, and creates implementation instance which will be later sent back to Business Logic.
This process of creating implementation instance, is called Factory Pattern.
Factory Pattern Steps
- Reads config
- Create implementation
- Returns its instance
(config files like .env, ConfigMap, compose.yaml etc.)
Dependency Injection
Once we have the implementation instance ready, it will finally inject the instance to Business Logic so it can work with the instance. And this action is called Dependency Injection. Dependency(Storage implementation instance) will be injected, to Business Logic.
Why Does This Separation Matter?
Because if we break down responsibilities to least common denominator, it will be so much easier for us to maintain an application.
By separating responsibilities, we now can:
- Replace infrastructure by changing config files.
- Upgrade the program of each responsibility easier.
- Maintain and test the program easier.
Final Thoughts
Cloud Native application design is about breaking down all the responsibilities into smaller units of responsibilities so applications become easier to maintain, scale and evolve.
Just as Separation of Concerns concept supported the following areas, Abstraction also supports the following:
- Replaceability
- Maintainability
- Testability
- Flexibility