Migrating a Python Django DRF Monolith to Microservices - Part 1: Planning the Migration

Ahmad W KhanAhmad W Khan
5 min read

This series walks through the step-by-step process of breaking a Django DRF monolith into microservices, containerizing the architecture, setting up CI/CD pipelines, and deploying on AWS using Kubernetes. Each part focuses on a specific phase.

Migrating from a monolithic architecture to microservices is a journey that requires careful planning and execution. This transformation, while complex, offers numerous benefits, including better scalability, maintainability, and resilience. In this article, we’ll lay the foundation for this migration, focusing on understanding the existing architecture, defining service boundaries, and preparing the application for modularization.

By the end of this article, you will have a clear roadmap for breaking down your Django DRF monolith into microservices, along with an understanding of the preliminary steps required for a successful migration.


Step 1: Understand the Current Monolith Architecture

1.1 Review the Monolith

A monolithic application is an architectural style where all components of the software are bundled together in a single codebase. For this guide, we’ll use a Robinhood-like trading platform as our case study.

  1. Core Features in the Monolith:

    • User Management:

      • Handles user registration, login, roles, and permissions.
    • Trading Engine:

      • Processes buy/sell orders and maintains transaction history.
    • Portfolio Management:

      • Tracks users’ investments and calculates returns.
    • Market Data:

      • Fetches and updates live stock market data from external APIs.
    • Notifications:

      • Sends notifications for trades, portfolio changes, and alerts.
  2. Sample Monolith Directory Structure:

     my_monolith/
     ├── authentication/
     │   ├── views.py
     │   ├── models.py
     │   ├── serializers.py
     │   ├── urls.py
     ├── trading/
     │   ├── views.py
     │   ├── models.py
     │   ├── serializers.py
     │   ├── urls.py
     ├── portfolio/
     │   ├── views.py
     │   ├── models.py
     │   ├── serializers.py
     │   ├── urls.py
     ├── market_data/
     ├── notifications/
     ├── manage.py
     ├── db.sqlite3
     └── requirements.txt
    
  3. Monolith Workflow:

    • The application uses Django DRF to handle API endpoints.

    • A single PostgreSQL database contains all tables for users, trades, portfolios, and notifications.

    • Each module communicates internally through shared models and function calls.

  4. Challenges with the Monolith:

    • Scalability: Scaling the trading engine requires scaling the entire application, wasting resources on unused components.

    • Deployment Risks: Updating one feature necessitates redeploying the entire system, increasing downtime risks.

    • Code Maintainability: Over time, a monolithic codebase can become tangled and harder to manage.


1.2 Challenges of the Monolith

Here’s a closer look at the pain points:

  1. Scalability:

    • Example: During peak trading hours, the trading engine may need more compute power. In a monolith, this requires scaling up the entire application, including unrelated modules like notifications.
  2. Code Coupling:

    • Changes in one module often ripple across the entire application, making it difficult to debug or extend.
  3. Team Collaboration:

    • With multiple teams working on the same codebase, conflicts arise, slowing down development.
  4. Technology Limitations:

    • Monoliths lock you into a single tech stack. Experimenting with new technologies for specific modules becomes challenging.

1.3 Current Technology Stack

  • Backend: Django REST Framework (DRF) for APIs.

  • Database: PostgreSQL.

  • Frontend: React (optional, if relevant).

  • Infrastructure: Deployed on a single server or basic container setup.


Step 2: Define Service Boundaries

2.1 Identify Domain-Specific Services

To convert the monolith into microservices, start by identifying logical groupings of features based on business domains. These will become independent services.

Proposed Services:

  1. User Service:

    • Responsibilities: User registration, login, roles, and permissions.

    • API Endpoints:

      • POST /users/: Create a new user.

      • POST /users/login/: Authenticate a user.

      • GET /users/:id/: Fetch user details.

  2. Trading Service:

    • Responsibilities: Manage trades and transaction history.

    • API Endpoints:

      • POST /trades/: Place a new trade.

      • GET /trades/:id/: Fetch trade details.

  3. Portfolio Service:

    • Responsibilities: Track investments and returns.

    • API Endpoints:

      • GET /portfolios/:user_id/: Get user portfolio.

      • POST /portfolios/: Update portfolio.

  4. Market Data Service:

    • Responsibilities: Fetch and store live stock market data.

    • API Endpoints:

      • GET /markets/:symbol/: Fetch market data for a stock.
  5. Notification Service:

    • Responsibilities: Send email and SMS alerts.

    • API Endpoints:

      • POST /notifications/: Send a notification.

2.2 Communication Between Services

In microservices, services need to communicate effectively while maintaining independence.

  1. Synchronous Communication:

    • Use REST APIs for direct communication.

    • Example: The Trading Service requests user authentication from the User Service.

  2. Asynchronous Communication:

    • Use a message broker like RabbitMQ or Kafka for event-driven interactions.

    • Example: The Trading Service publishes an event after a trade is executed. The Notification Service consumes this event and sends an alert.


2.3 Data Ownership

Each service should own its data, ensuring autonomy. This eliminates bottlenecks caused by shared databases.

Example Schema:

  • User Service Database:

    • Tables: users, roles.
  • Trading Service Database:

    • Tables: orders, transactions.
  • Portfolio Service Database:

    • Tables: portfolios, investments.

Step 3: Refactoring the Monolith

Refactoring involves isolating parts of the monolith into independent services.

3.1 Extracting the User Service

  1. Setup a New Django Project:

    • Create a new Django project for the User Service:

        django-admin startproject user_service
        cd user_service
        pip install djangorestframework
      
  2. Database Setup:

    • Configure the settings to point to a new PostgreSQL database:

        DATABASES = {
            'default': {
                'ENGINE': 'django.db.backends.postgresql',
                'NAME': 'user_service_db',
                'USER': 'dbuser',
                'PASSWORD': 'password',
                'HOST': 'localhost',
                'PORT': '5432',
            }
        }
      
  3. Define Models:

     from django.contrib.auth.models import AbstractUser
     from django.db import models
    
     class User(AbstractUser):
         phone_number = models.CharField(max_length=15, unique=True)
    
  4. Build APIs:

     from rest_framework.viewsets import ModelViewSet
     from .models import User
     from .serializers import UserSerializer
    
     class UserViewSet(ModelViewSet):
         queryset = User.objects.all()
         serializer_class = UserSerializer
    
  5. Test Locally:

    • Run migrations and start the server:

        python manage.py migrate
        python manage.py runserver
      
    • Test the APIs using Postman or curl.


3.2 Repeat for Other Services

Use the same methodology to extract the Trading Service, Portfolio Service, and others. Ensure each service is self-contained with its own database and logic.


3.3 Shared Libraries

  1. Extract Common Code:

    • Example: Token generation and verification:

        import jwt
      
        def generate_token(data):
            return jwt.encode(data, 'secret', algorithm='HS256')
      
  2. Package for Reuse:

    • Distribute as a Python package using setup.py.

Step 4: Prepare for Communication Between Services

Document APIs with tools like Swagger or Postman. For example:

Swagger Documentation for Login API:

paths:
  /api/users/login:
    post:
      summary: "Authenticate a user"
      responses:
        200:
          description: "JWT token issued"

Conclusion

You’ve identified service boundaries, refactored at least one module into a microservice, and planned inter-service communication. This foundation sets the stage for Part 2, where we’ll dive into Dockerizing these services for deployment.

Thanks for reading!

0
Subscribe to my newsletter

Read articles from Ahmad W Khan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ahmad W Khan
Ahmad W Khan