Platform Agnosticity in Microservices
Platform agnosticity means keeping a microservice’s business logic independent of the infrastructure it runs on. A service should be able to change its hosting environment or infrastructure provider without rewriting its core business behavior.
For example, a document service might store files in Azure Blob Storage when deployed to Azure and Amazon S3 when deployed to AWS. The document-processing logic stays the same; the storage implementation and deployment configuration change.
This does not mean .NET and Java share executable code. Both can apply the same architectural pattern independently.
The Basic Pattern
Use three parts:
- Interface: Defines a capability the business logic needs, such as saving a document.
- Concrete implementations: Provide that capability using specific technologies.
- Deployment-time configuration: Selects which implementation the service creates at startup.
flowchart TD
Config["Deployment configuration<br/>STORAGE_PROVIDER"] --> Composition["Startup composition root"]
Composition --> Azure["AzureBlobStorage"]
Composition --> S3["S3Storage"]
Business["DocumentService<br/>Business logic"] --> Interface["IDocumentStorage / DocumentStorage<br/>Storage interface"]
Interface -. implemented by .-> Azure
Interface -. implemented by .-> S3
Azure --> AzureSDK["Azure Blob Storage SDK"]
S3 --> S3SDK["AWS S3 SDK"]
AzureSDK --> AzurePlatform["Azure Blob Storage"]
S3SDK --> AWSPlatform["Amazon S3"]
The business logic depends on the interface. Provider-specific SDKs and behavior belong inside the concrete implementations. Deployment configuration selects the implementation during startup.
.NET Example
Define the interface and implementations
The following implementations are illustrative: the console messages mark where actual provider SDK calls would go.
public interface IDocumentStorage
{
Task SaveAsync(string name, byte[] content);
}
public sealed class AzureBlobStorage : IDocumentStorage
{
public Task SaveAsync(string name, byte[] content)
{
// Use the Azure Blob Storage SDK here.
Console.WriteLine($"Saving {name} to Azure Blob Storage");
return Task.CompletedTask;
}
}
public sealed class S3Storage : IDocumentStorage
{
public Task SaveAsync(string name, byte[] content)
{
// Use the AWS S3 SDK here.
Console.WriteLine($"Saving {name} to Amazon S3");
return Task.CompletedTask;
}
}
Keep business logic independent of the provider
Constructor injection gives the service its storage dependency. The service does not need to know which implementation it receives.
public sealed class DocumentService
{
private readonly IDocumentStorage _storage;
public DocumentService(IDocumentStorage storage)
{
_storage = storage;
}
public Task UploadAsync(string name, byte[] content)
{
// Provider-independent validation and business rules go here.
return _storage.SaveAsync(name, content);
}
}
Select the implementation at startup
In an ASP.NET Core application's Program.cs:
var builder = WebApplication.CreateBuilder(args);
var provider = builder.Configuration["STORAGE_PROVIDER"]
?? throw new InvalidOperationException(
"STORAGE_PROVIDER must be configured.");
switch (provider.ToLowerInvariant())
{
case "azure":
builder.Services.AddSingleton<IDocumentStorage, AzureBlobStorage>();
break;
case "s3":
builder.Services.AddSingleton<IDocumentStorage, S3Storage>();
break;
default:
throw new InvalidOperationException(
$"Unsupported storage provider: {provider}");
}
builder.Services.AddScoped<DocumentService>();
var app = builder.Build();
// Map application endpoints that use DocumentService here.
app.Run();
ASP.NET Core's default configuration includes environment variables, allowing the deployment to supply STORAGE_PROVIDER. See ASP.NET Core configuration.
Java Example
Define the interface and implementations
These implementations use the same illustrative placeholders as the .NET example.
public interface DocumentStorage {
void save(String name, byte[] content);
}
public final class AzureBlobStorage implements DocumentStorage {
@Override
public void save(String name, byte[] content) {
// Use the Azure Blob Storage SDK here.
System.out.println("Saving " + name + " to Azure Blob Storage");
}
}
public final class S3Storage implements DocumentStorage {
@Override
public void save(String name, byte[] content) {
// Use the AWS S3 SDK here.
System.out.println("Saving " + name + " to Amazon S3");
}
}
Each public Java type belongs in its own matching .java file.
Keep business logic independent of the provider
public final class DocumentService {
private final DocumentStorage storage;
public DocumentService(DocumentStorage storage) {
this.storage = storage;
}
public void upload(String name, byte[] content) {
// Provider-independent validation and business rules go here.
storage.save(name, content);
}
}
Select the implementation at startup
This framework-independent factory uses constructor injection without requiring a dependency injection container.
public final class ServiceFactory {
public static DocumentService createDocumentService() {
String provider = System.getenv("STORAGE_PROVIDER");
if (provider == null || provider.isBlank()) {
throw new IllegalStateException(
"STORAGE_PROVIDER must be configured.");
}
DocumentStorage storage;
if ("azure".equalsIgnoreCase(provider)) {
storage = new AzureBlobStorage();
} else if ("s3".equalsIgnoreCase(provider)) {
storage = new S3Storage();
} else {
throw new IllegalStateException(
"Unsupported storage provider: " + provider);
}
return new DocumentService(storage);
}
}
The application's startup code calls ServiceFactory.createDocumentService() and passes the result to its request handlers. Java reads the deployment variable through System.getenv.
Deployment-Time Configuration
The same environment variable works for either service:
# Configuration for a deployment using Azure Blob Storage
env:
- name: STORAGE_PROVIDER
value: azure
# Configuration for a deployment using Amazon S3
env:
- name: STORAGE_PROVIDER
value: s3
These are Kubernetes container configuration fragments. Other deployment systems can supply the same environment variable.
Each service can reuse its own application image across these deployments, provided the image already contains both implementations and their dependencies. At startup, configuration determines which implementation is injected.
Actual storage adapters also need provider-specific settings, such as bucket or container names, endpoints, and credentials or workload identity. Deployment supplies those settings; the business logic remains unaware of them.
What This Achieves
- Portability: Infrastructure changes are concentrated in adapters and deployment configuration.
- Testability: Tests can inject an in-memory storage implementation.
- Maintainability: Provider SDK code stays separate from business rules.
- Consistency: .NET and Java services follow the same design, even with different frameworks.
An interface alone does not guarantee portability. Implementations must satisfy the same behavioral contract, including error handling and overwrite behavior. Moving platforms may still require data migration and changes to identity, networking, and deployment infrastructure.
Relationships
flowchart LR nplatform_agnosticity_58A71A29["Design: Platform Agnosticity in Microservices"]