April 21st, 2024
{ Engineering }
Optimising Configuration Management: A case study on database-driven solutions.
At Cowrywise, we offer a diverse range of products, each needing specific configurations on the backend. Initially, we stored these configuration details using environment variables. However, as our product offerings grew, repeatedly updating these variables became increasingly cumbersome and time-consuming.
Additionally, certain situations, such as third-party service downtime or switching to alternative providers, required us to automate configuration updates without restarting the application.
Drawbacks of the Previous Approach (Environment Variables).
Whenever we needed to change our configuration, a backend engineer had to log into the server, manually update the relevant environment variables, and restart the server for the changes to take effect. This tedious process introduced the risk of human error and potential downtime during server restarts. Furthermore, it did not provide a mechanism to update configurations dynamically based on specific situations.
The New Configuration Management System.
To streamline the configuration management process and enhance maintainability, we implemented a dedicated configuration model that stores all configuration parameters in the database. This approach leverages the singleton pattern, ensuring consistent and centralised access to the configuration data across the entire application.
The configuration model acts as a single source of truth for all configuration-related data, and relevant stakeholders can also expose and utilise API endpoints on internal dashboards to change these configurations instead of solely depending on engineers.
Backend engineers can also update the configurations directly in the database, eliminating the need for manual intervention on the server and server restarts.
Moreover, the model allows for dynamic updates to configurations based on specific situations in the application, such as third-party service downtime or the need to switch to alternative configuration values depending on the state of objects in an execution context.
Implementation Details.
We implemented the singleton pattern using a base SingletonModel class that enforces a single instance of the configuration model in the database:
from django.db import models
class SingletonModel(models.Model):
class Meta:
abstract = True
def save(self, *args, **kwargs):
self.full_clean()
self.pk = 1
super(SingletonModel, self).save(*args, **kwargs)
@classmethod
def load(cls):
obj, _ = cls.objects.get_or_create(pk=1)
return obj
The Configuration model, which stores the actual configuration parameters, inherits from the SingletonModel:
class Configuration(SingletonModel):
default_spawn_planet = models.CharField(max_length=255)
To access the configuration data, we call the load method on the Configuration model:
from your_app.models import Configuration
config = Configuration.load()
default_spawn_planet = config.default_spawn_planet
if default_spawn_planet == "Mars":
# Perform spawn-point activities with Mars context
This implementation ensures that only a single instance of the configuration model exists in the database, providing a centralised and consistent source of configuration data throughout the application.
Configuration Management API.
We exposed the Configuration model through Django REST Framework (DRF) API endpoints to facilitate dynamic updates and provide a centralised interface for managing configurations. This approach enables authorised users, such as internal stakeholders and dashboard applications, to retrieve and modify configuration parameters without direct access to the database or server.
A dedicated ConfigurationViewSet, which handles the retrieval and modification of configuration data:
from rest_framework import viewsets
from .models import Configuration
from .serializers import ConfigurationSerializer
class ConfigurationViewSet(viewsets.ModelViewSet):
queryset = Configuration.objects.all()
serializer_class = ConfigurationSerializer
A ConfigurationSerializer responsible for serialising and deserialising the Configuration model instances to and from JSON format, enabling easy data transfer over the API.
from rest_framework import serializers
from .models import Configuration
class ConfigurationSerializer(serializers.ModelSerializer):
class Meta:
model = Configuration
fields = '__all__'
To register the routes for the ConfigurationViewSet, you would typically define them in your project’s urls.py file.
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from your_app.views import ConfigurationViewSet
router = DefaultRouter()
router.register('configurations', ConfigurationViewSet, basename='configuration')
urlpatterns = [
# ... other URL patterns ...
path('api/', include(router.urls)),
]
DRF automatically generates the following endpoints:
- GET /api/configurations/ – Retrieve the current configuration
- PUT /api/configurations/1/ – Update the configuration
- PATCH /api/configurations/1/ – Partially update the configuration
To update the default_spawn_planet config, a stakeholder would send a PATCH request to /api/configurations/1 with the following request body:
{
"default_spawn_planet": "Jupiter"
}
Exposing the Configuration model through an API offers several advantages, including centralised management of configurations through a single endpoint, dynamic updates to configuration parameters without requiring application restarts or server downtime, integration with internal dashboards for retrieving and updating configurations, and scalability to handle increased configuration management demands as the application grows.
For security reasons, only authorised users with specific roles and permissions can access and modify the configuration endpoint.
Benefits and Lessons Learned.
Throughout this transition, we gained valuable insights into the importance of maintainable, scalable, and adaptable configurations:
- Separation of Concerns: By separating the configuration data from the application code, we achieved better modularity and easier maintenance.
- Centralised Management: A centralised configuration store simplifies updates and reduces the risk of inconsistencies across different parts of the application.
- Auditability: Storing configurations in the database provides a clear audit trail, allowing us to track changes and revert if necessary.
- Dynamic Adaptability: The ability to update configurations dynamically based on specific situations enhances the system’s resilience and flexibility and ensures uninterrupted service delivery.
Conclusion.
By adopting a database-driven configuration model, we successfully streamlined the management of product configurations. This approach simplifies the update process and enhances maintainability, scalability, auditability, and adaptability. As we continue to grow and introduce new products, our configuration management system is well-equipped to handle increasing complexity, ensure a seamless experience for all stakeholders, and provide uninterrupted service delivery to our valued customers.