Switch Between Monolith And Microservices

Your stack:

Goal

Start with two Python modules in a single project running as a monolith, then extract one into a separate microservice using Graftcode. After that one-time setup, switch freely between monolith and microservice by changing a single configuration value - zero code changes.

What You'll See

  • Create two Python modules in the same project - a price calculator and a billing service that calls it directly.
  • Host both in a single container as a monolith.
  • Extract the price calculator into its own container as a standalone microservice.
  • Connect the billing service to it through a Graft.
  • Switch between monolith and microservice by changing one environment variable - no code changes from that point on.

Prerequisites

  • Docker installed and running
  • Python installed locally (for pip commands)

Step 1. Create a project folder

Create a new project folder and initialize a Python project:

mkdir py-energy-platform
cd py-energy-platform

Create a pyproject.toml:

[project]
name = "energy-platform"
version = "1.0.0"
requires-python = ">=3.8"
description = "Energy platform - switches between monolith and microservice deployments"

Step 2. Write the price calculator module

Create src/price_calculator.py:

import random


class EnergyPriceCalculator:
    @staticmethod
    def get_price() -> int:
        return random.randint(100, 104)

Step 3. Write the billing service

Create src/billing_service.py:

from price_calculator import EnergyPriceCalculator


class BillingService:
    @staticmethod
    def calculate_bill(kwh_used: int) -> int:
        price = EnergyPriceCalculator.get_price()
        return kwh_used * price

A regular import - the billing service imports the price calculator directly as a local module. No Graftcode involved yet.

Step 4. Host as a monolith

Create a Dockerfile in the project root:

FROM python:3.13-bookworm

WORKDIR /usr/app

COPY . /usr/app/

RUN apt-get update \
 && apt-get install -y wget \
 && wget -O /usr/app/gg.deb https://github.com/grft-dev/graftcode-gateway/releases/latest/download/gg_linux_amd64.deb \
 && dpkg -i /usr/app/gg.deb \
 && rm /usr/app/gg.deb \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*

EXPOSE 80
EXPOSE 81

CMD ["gg", "--modules", "./src/"]

Build and run:

docker build --no-cache --pull -t py-energy-platform:test .
docker run -d -p 80:80 -p 81:81 --name energy_platform py-energy-platform:test

gg (Graftcode Gateway) scans the ./src/ directory, discovers both modules, and exposes all their public methods. Port 80 handles service calls, port 81 serves Graftcode Vision.

Open http://localhost:81/GV and try calling BillingService.calculate_bill with a value like 250. You'll see both BillingService and EnergyPriceCalculator listed with all their methods.

At this point, everything runs inside one container - both modules share a single process. This is your monolith.

Step 5. Extract the price calculator as a separate microservice

Now let's say the price calculator needs to scale independently, or another team wants to own it. We'll extract it into its own container.

Create Dockerfile.priceCalculator in the project root:

FROM python:3.13-bookworm

WORKDIR /usr/app

COPY . /usr/app/

RUN apt-get update \
 && apt-get install -y wget \
 && wget -O /usr/app/gg.deb https://github.com/grft-dev/graftcode-gateway/releases/latest/download/gg_linux_amd64.deb \
 && dpkg -i /usr/app/gg.deb \
 && rm /usr/app/gg.deb \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*

EXPOSE 90
EXPOSE 91

CMD ["gg", "--modules", "/usr/app/src/price_calculator.py", "--httpPort", "91", "--port", "90", "--TCPServer", "--tcpPort=9092"]

Build and run the price calculator as a standalone service:

docker build --no-cache --pull -f Dockerfile.priceCalculator -t price-calculator-py:test .
docker network create graftcode_demo
docker run -d --network graftcode_demo -p 90:90 -p 91:91 -p 9092:9092 --name price_calculator price-calculator-py:test

Open http://localhost:91/GV - the price calculator is now an independent service with its own Graftcode Vision. You can see EnergyPriceCalculator.get_price listed with its return type.

Step 6. Connect the billing service through a Graft

Now that the price calculator runs on its own gateway, install its Graft - the strongly-typed client that Graftcode generates automatically.

From Graftcode Vision at http://localhost:91/GV, select PyPI and copy the generated install command. Note that the --extra-index-url address shown in your Graftcode Vision interface may be different than the example provided below.

pip install hypertube-python-sdk
pip install --target=./lib --extra-index-url http://localhost:91/simple/ graft-pypi-energypricecalculator

Note

The exact package name and registry URL are shown in Graftcode Vision - copy them from there. hypertube-python-sdk is still required for this example today, but that extra step is temporary. The --target=./lib flag installs packages into a local lib/ directory so they get copied into the container alongside your project - similar to how npm install stores packages in node_modules/.

Update src/billing_service.py to use the Graft instead of the direct import:

import os
from graft_pypi_energypricecalculator import GraftConfig, EnergyPriceCalculator

GraftConfig.set_config(os.environ.get("GRAFT_CONFIG"))


class BillingService:
    @staticmethod
    async def calculate_bill(kwh_used: int) -> int:
        price = await EnergyPriceCalculator.get_price()
        return kwh_used * price

This is the only code change in the entire tutorial. The billing service now reads its configuration from the GRAFT_CONFIG environment variable and has no knowledge of whether the price calculator runs in-process or on a remote host. From this point on, switching between monolith and microservice is purely a configuration change.

Step 7. Run as a microservice

Stop the monolith container, rebuild the image with the updated code, and run the billing service pointing at the remote price calculator:

docker stop energy_platform
docker rm energy_platform
docker build --no-cache --pull -t py-energy-platform:test .
docker run -d --network graftcode_demo \
  -e GRAFT_CONFIG="name=graft-pypi-energypricecalculator;host=price_calculator:9092;runtime=python;modules=/usr/app/src" \
  -e PYTHONPATH=/usr/app/lib \
  -p 80:80 -p 81:81 \
  --name energy_platform py-energy-platform:test

Open http://localhost:81/GV and call BillingService.calculate_bill with 250. Same method, same result - but the price calculation now happens over the network in a separate container.

Step 8. Switch back to monolith

Want to go back to a monolith? Stop and restart with host=inMemory instead:

docker stop energy_platform
docker rm energy_platform
docker run -d \
  -e GRAFT_CONFIG="name=graft-pypi-energypricecalculator;host=inMemory;runtime=python;modules=/usr/app/src" \
  -e PYTHONPATH=/usr/app/lib \
  -p 80:80 -p 81:81 \
  --name energy_platform py-energy-platform:test

Compare the two configurations side by side:

# Monolith (in-process)
name=graft-pypi-energypricecalculator;host=inMemory;runtime=python;modules=/usr/app/src

# Microservice (remote)
name=graft-pypi-energypricecalculator;host=price_calculator:9092;runtime=python;modules=/usr/app/src

Note

We're still working on the best way to pass the configuration so that it's intuitive and user friendly.

Same Docker image, same code - just a different environment variable. You can switch back and forth as many times as you need.

Step 9. Prove the microservice call goes over the network

Switch back to microservice mode to verify the call is truly remote:

docker stop energy_platform
docker rm energy_platform
docker run -d --network graftcode_demo \
  -e GRAFT_CONFIG="name=graft-pypi-energypricecalculator;host=price_calculator:9092;runtime=python;modules=/usr/app/src" \
  -e PYTHONPATH=/usr/app/lib \
  -p 80:80 -p 81:81 \
  --name energy_platform py-energy-platform:test

Stop the price calculator:

docker stop price_calculator

Call calculate_bill in Graftcode Vision - you'll see a connection error because the remote service is down.

Start it again:

docker start price_calculator

The method works again. The code never changed - only the deployment topology did.

Everything above works without any account - perfect for learning and local development. When you're ready for real-world usage, create a free account at portal.graftcode.com, set up a project, and copy its Project Key.

Then pass the key when starting your gateways:

CMD ["gg", "--modules", "./src/", "--projectKey", "YOUR_PROJECT_KEY"]

A Project Key gives you:

  • Stable registry URL - consumers always find and update your Graft through a permanent address, so install commands don't change when you redeploy.
  • Portal visibility - see all your gateways and exposed services in one place at gateways.graftcode.com.
  • Access control - decide who can download your Grafts using package manager authentication and permissions.