Skip to Content

Adding a Service

This guide walks through creating a new Python microservice in the AEGIS monorepo, following the same patterns used by existing services.

Directory Structure

Every Python service follows this layout:

services/my-service/ ├── pyproject.toml # Poetry config + pytest settings ├── poetry.lock # Locked dependencies ├── src/ │ └── my_service/ # Python package (underscore, not hyphen) │ ├── __init__.py │ ├── main.py # FastAPI app with lifespan │ ├── config.py # Settings from environment variables │ └── ... # Business logic modules └── tests/ ├── __init__.py ├── conftest.py # Shared test fixtures └── test_*.py # Test files

The directory name uses hyphens (my-service) but the Python package name uses underscores (my_service). This follows the convention of every existing AEGIS service.

Step 1: Create the pyproject.toml

[tool.poetry] name = "my-service" version = "0.1.0" description = "Description of what this service does in AEGIS" authors = ["AEGIS Team"] packages = [{include = "my_service", from = "src"}] [tool.poetry.dependencies] python = "^3.12" fastapi = "^0.115" uvicorn = {version = "^0.34", extras = ["standard"]} aegis-shared = {path = "../../shared", develop = true} [tool.poetry.group.dev.dependencies] pytest = "^8.0" pytest-asyncio = "^0.23" httpx = "^0.28" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] asyncio_mode = "auto"

Key points:

  • The shared library is always included as a path dependency: {path = "../../shared", develop = true}
  • asyncio_mode = "auto" is required for async tests
  • Add additional dependencies as needed (e.g., asyncpg for PostgreSQL, redis for Redis)

Step 2: Create the FastAPI Application

# src/my_service/main.py import logging from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException from aegis_shared.db.postgres import PostgresPool from my_service.config import settings logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # ── Globals set during lifespan ──────────────────────────────────────── pg: PostgresPool @asynccontextmanager async def lifespan(app: FastAPI): global pg pg = PostgresPool(dsn=settings.DATABASE_URL) await pg.connect() logger.info("My service started — PostgreSQL connected") yield await pg.disconnect() logger.info("My service stopped") app = FastAPI( title="AEGIS My Service", version="0.1.0", description="Description of what this service does", lifespan=lifespan, ) # ── Health ───────────────────────────────────────────────────────────── @app.get("/health") async def health(): return {"status": "ok", "service": "my-service"} # ── Endpoints ────────────────────────────────────────────────────────── # Add your service-specific endpoints here if __name__ == "__main__": import uvicorn uvicorn.run(app, host=settings.HOST, port=settings.PORT)

Step 3: Create the Config Module

# src/my_service/config.py import os class Settings: HOST: str = os.getenv("HOST", "0.0.0.0") PORT: int = int(os.getenv("PORT", "8010")) DATABASE_URL: str = os.getenv( "DATABASE_URL", "postgresql://aegis:aegis_local@localhost:5432/aegis", ) REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379") settings = Settings()

Step 4: Create the init.py Files

mkdir -p services/my-service/src/my_service touch services/my-service/src/my_service/__init__.py mkdir -p services/my-service/tests touch services/my-service/tests/__init__.py

Step 5: Set Up Tests

Create a conftest.py with appropriate fixtures. The pattern depends on your service’s infrastructure needs.

For a PostgreSQL-backed service:

# tests/conftest.py from contextlib import asynccontextmanager from typing import Any import pytest from fastapi.testclient import TestClient class FakePostgresPool: """In-memory PostgreSQL mock.""" def __init__(self): self.data: dict[str, list[dict[str, Any]]] = {} async def execute(self, query: str, *args) -> str: return "OK" async def fetch(self, query: str, *args) -> list[dict[str, Any]]: return [] async def fetchrow(self, query: str, *args) -> dict[str, Any] | None: return None async def fetchval(self, query: str, *args): return None async def connect(self): pass async def disconnect(self): pass @pytest.fixture def fake_pg(): return FakePostgresPool() @pytest.fixture def app_client(fake_pg): import my_service.main as main_module main_module.pg = fake_pg @asynccontextmanager async def noop_lifespan(app): yield main_module.app.router.lifespan_context = noop_lifespan return TestClient(main_module.app)

Create a basic test file:

# tests/test_api.py class TestHealth: def test_health(self, app_client): r = app_client.get("/health") assert r.status_code == 200 assert r.json()["service"] == "my-service"

Step 6: Install Dependencies and Verify

cd services/my-service poetry install poetry run pytest -v poetry run uvicorn my_service.main:app --port 8010

Step 7: Add to the API Gateway

The Go API gateway at services/api-gateway/cmd/gateway/main.go routes requests to backend services. To add your new service, add a route mapping.

The gateway forwards requests matching URL path prefixes to the appropriate backend. You need to add an entry that maps a path prefix to your service’s port:

/api/v1/my-feature -> http://localhost:8010

The exact mechanism for adding routes depends on the gateway’s current implementation. Read services/api-gateway/cmd/gateway/main.go to understand the routing table format before making changes.

Step 8: Add to start-all.sh

Add your service to infrastructure/scripts/start-all.sh:

# Add after the existing service start commands start_python_service "my-service" "my_service.main:app" 8010 "My Service"

Also add the port to the stop function’s cleanup list:

for port in 8000 8001 8002 8003 8004 8005 8006 8007 8009 8010; do lsof -ti:$port | xargs kill -9 2>/dev/null || true done

Step 9: Add Shared Model Imports (if needed)

If your service needs to use or define shared models:

  1. Using existing models — import from the shared library:

    from aegis_shared.models.common import AegisBase, TenantEntity from aegis_shared.models.oilgas import Well, Lease
  2. Adding new models — create or extend files in shared/src/aegis_shared/models/:

    See the Shared Models page for details on adding new models.

Step 10: Add Database Tables (if needed)

If your service needs new database tables:

  1. Create a migration SQL file in infrastructure/docker/postgres/:

    infrastructure/docker/postgres/0XX_my_service_tables.sql
  2. Add the volume mount in docker-compose.yml:

    volumes: - ./infrastructure/docker/postgres/0XX_my_service_tables.sql:/docker-entrypoint-initdb.d/0XX_my_service_tables.sql
  3. For the init script to run, you need a fresh volume:

    docker compose down rm -rf docker-volumes/postgres docker compose up -d postgres

For ongoing schema changes after initial setup, use Alembic migrations per-service rather than modifying init scripts.

Checklist Summary

StepActionFile/Location
1Create directory structureservices/my-service/
2Create pyproject.tomlservices/my-service/pyproject.toml
3Create FastAPI appservices/my-service/src/my_service/main.py
4Create configservices/my-service/src/my_service/config.py
5Create __init__.py filessrc/my_service/__init__.py, tests/__init__.py
6Create test fixturesservices/my-service/tests/conftest.py
7Create test filesservices/my-service/tests/test_*.py
8Install and verifypoetry install && poetry run pytest
9Add gateway routeservices/api-gateway/cmd/gateway/main.go
10Add to startup scriptinfrastructure/scripts/start-all.sh
11Add DB tables (if needed)infrastructure/docker/postgres/0XX_*.sql
12Export shared models (if needed)shared/src/aegis_shared/models/
Last updated on