Skip to content

Example Usage: Microservices Ecosystem

This guide demonstrates pyrig's power in a real-world scenario: building a standardized microservices ecosystem across multiple projects.

Scenario

You're building multiple Python microservices that need:

  • Consistent standards across all services
  • Custom shared configurations (logging, monitoring, security)
  • Automatic synchronization when standards change
  • Self-healing infrastructure via autouse fixtures

Architecture Overview

graph TD A[pyrig] --> B[service-base] B --> C[auth-service] B --> D[payment-service] B --> E[notification-service] style A fill:#a8dadc,stroke:#333,stroke-width:2px,color:#000 style B fill:#f4a261,stroke:#333,stroke-width:2px,color:#000 style C fill:#90be6d,stroke:#333,stroke-width:2px,color:#000 style D fill:#90be6d,stroke:#333,stroke-width:2px,color:#000 style E fill:#90be6d,stroke:#333,stroke-width:2px,color:#000

Dependency chain: pyrigservice-baseauth-service, payment-service, notification-service

Step 1: Create Base Package

Create service-base that extends pyrig with shared standards:

# Create repository
gh repo create myorg/service-base --public  # Or private if you want
git clone https://github.com/myorg/service-base.git
cd service-base

# Initialize with pyrig
uv init
uv add pyrig
uv run pyrig init

This creates a complete project structure with all pyrig's defaults.

Step 2: Add Custom Config File

Add a shared logging configuration that all microservices will inherit.

Create: service_base/rig/configs/logging_config.py

"""Shared logging configuration for all services."""

from pathlib import Path
from pyrig.rig.configs.base.base import ConfigDict
from pyrig.rig.configs.base.yaml import YamlConfigFile


class LoggingConfigFile(YamlConfigFile):
    """Logging configuration for all microservices."""


    def parent_path(self) -> Path:
        """Place in config/ directory."""
        return Path("config")


    def _configs(self) -> ConfigDict:
        """Required logging configuration."""
        return {
            "logging": {
                "version": 1,
                "formatters": {
                    "json": {
                        "class": "pythonjsonlogger.jsonlogger.JsonFormatter",
                        "format": "%(name)s %(levelname)s %(message)s"
                    }
                },
                "handlers": {
                    "console": {
                        "class": "logging.StreamHandler",
                        "formatter": "json",
                        "level": "INFO"
                    }
                },
                "root": {
                    "level": "INFO",
                    "handlers": ["console"]
                }
            }
        }

What happens:

  • File created at config/logging.yaml when you run uv run pyrig mkroot
  • Automatically discovered via pyrig's ConfigFile discovery system
  • Inherited by all services that depend on service-base

Step 3: Override MkDocs Theme

Customize documentation theme with custom branding.

Create: service_base/rig/configs/docs/mkdocs.py

"""Custom-branded MkDocs configuration."""

from pathlib import Path
from pyrig.rig.configs.base.base import ConfigDict
from pyrig.rig.configs.docs.mkdocs import MkdocsConfigFile as BaseMkdocsCF


class MkdocsConfigFile(BaseMkdocsCF):
    """Custom-branded documentation theme."""


    def _configs(self) -> ConfigDict:
        """Override theme with custom colors."""
        config = super()._configs()

        # Add custom branding
        config["theme"]["palette"] = [
            {
                "scheme": "slate",
                "primary": "deep purple",  # Brand color
                "accent": "amber",         # Brand accent
                "toggle": {
                    "icon": "material/brightness-4",
                    "name": "Light mode",
                },
            },
            {
                "scheme": "default",
                "primary": "deep purple",
                "accent": "amber",
                "toggle": {
                    "icon": "material/brightness-7",
                    "name": "Dark mode",
                },
            },
        ]

        # Add custom logo
        config["theme"]["logo"] = "assets/logo.png"
        config["theme"]["favicon"] = "assets/favicon.ico"

        return config

Key mechanism:

  • Subclass pyrig's MkdocsConfigFile with the same name
  • Leaf class override: pyrig's discovery system automatically keeps only the most-derived (leaf) class when multiple classes are found in the inheritance chain
  • All microservices automatically get custom branding

Step 4: Adjust Pyproject Settings

Add shared dependencies and settings.

Create: service_base/rig/configs/pyproject.py

"""Base pyproject.toml with additional dependencies."""

from pyrig.rig.configs.base.base import ConfigDict
from pyrig.rig.configs.pyproject import PyprojectConfigFile as BasePyprojectCF


class PyprojectConfigFile(BasePyprojectCF):
    """Base pyproject with monitoring and logging."""


    def dependencies(self) -> list[str]:
        """Add shared runtime dependencies."""
        deps = super().dependencies()
        return [
            *deps,
            "python-json-logger>=2.0.0",  # JSON logging
            "prometheus-client>=0.19.0",   # Metrics
            "sentry-sdk>=1.40.0",          # Error tracking
        ]


    def _configs(self) -> ConfigDict:
        """Add shared tool configs."""
        config = super()._configs()

        # Add custom ruff rules for shared standards
        config["tool"]["ruff"]["lint"]["ignore"].extend([
            "T201",  # Allow print statements in microservices
        ])

        # Adjust coverage threshold for microservices
        config["tool"]["pytest"]["ini_options"]["addopts"] = (
            "--cov=. --cov-report=term-missing --cov-fail-under=85"
        )

        return config

What this does:

  • Adds dependencies to all microservices automatically
  • Adjusts linting rules across all services
  • Lowers coverage to 85% (more realistic for microservices)

Step 5: Create First Microservice

Now create auth-service that depends on service-base:

# Create repository
gh repo create myorg/auth-service
git clone https://github.com/myorg/auth-service.git
cd auth-service

# Initialize with pyrig
uv init
# service base brings pyrig bc it is build with it
uv add service-base
uv run pyrig init

Note: If your repos are private you will need tokens and stuff to do uv add. If you manage a package ecosystem for pypi then you can use uv add like usual

What gets created automatically:

auth-service/
├── config/
│   └── logging.yaml               # ✓ From service-base
├── mkdocs.yml                      # ✓ Custom-branded theme
├── pyproject.toml                  # ✓ With shared dependencies
├── auth_service/
│   ├── rig/
│   │   ├── configs/               # ✓ Can add service-specific configs
│   │   ├── cli/                   # ✓ Service-specific commands
│   │   └── tests/                 # ✓ Service-specific fixtures
│   └── src/                       # ✓ Your auth logic here
└── tests/                          # ✓ Mirrored structure

See the full generated project tree at: Getting Started Documentation

All shared standards applied automatically!

Step 6: The Magic - Automatic Synchronization

Here's where pyrig shines. When you update service-base, all services heal themselves.

Update Shared Standards

In service-base, add a security requirement:

Update: service_base/rig/configs/pyproject.py


def dependencies(self) -> list[str]:
    """Add shared runtime dependencies."""
    deps = super().dependencies()
    return [
        *deps,
        "python-json-logger>=2.0.0",
        "prometheus-client>=0.19.0",
        "sentry-sdk>=1.40.0",
        "cryptography>=42.0.0",  # NEW: Shared security requirement
    ]

Commit and release:

cd service-base
git add .
git commit -m "Add cryptography requirement"
git push
# GitHub Actions automatically releases new version

Services Auto-Heal

In auth-service:

# Update service-base dependency
uv add service-base --upgrade

# Run tests (triggers autouse fixtures)
uv run pytest
# or just do
uv run pyrig mkroot

Note: A nice thing is that this can not go unnoticed. Lets say you add a change like described above here. Then your pipelines in Github that pyrig creates for you will fail because the ConfigFile is not correct anymore. So the responsible autouse fixture will fail and GitHub will notify you because the health-check workflow failed. Then you just need to open your repo in your IDE, run pyrig mkroot or pytest review the changes and commit and push.

What happens automatically:

  1. assert_root_is_correct fixture runs (session-level autouse)
  2. Detects pyproject.toml is missing cryptography>=42.0.0
  3. Calls ConfigFile.validate_subclasses() to fix it
  4. Adds the missing dependency to the file
  5. Tests fails pytest will raise with a descriptive error message of which Config Files were not correct. Then you can check the git diff of what changed

Note: We decided against it to just autoadd these changes as that would be too much magic and also things possibly can interfere with other customisations you made, so yprig raises to not do things silently. The only changes pyrig silently autoadds are uv lock --upgrade in the release workflow, which keeps your dependencies automatically up to date even when you do not work on a project for a while.

Only minimal manual intervention needed!

Step 7: Service-Specific Customization

Each service can still customize while keeping shared standards.

In auth-service, add service-specific config:

Create: auth_service/rig/configs/auth_config.py

"""Auth service specific configuration."""

from pathlib import Path
from pyrig.rig.configs.base.base import ConfigDict
from pyrig.rig.configs.base.yaml import YamlConfigFile


class AuthConfigFile(YamlConfigFile):
    """JWT and OAuth configuration."""


    def parent_path(self) -> Path:
        return Path("config")


    def _configs(self) -> ConfigDict:
        return {
            "auth": {
                "jwt_algorithm": "RS256",
                "token_expiry": 3600,
                "oauth_providers": ["google", "github"]
            }
        }

Result: auth-service has both:

  • ✓ Shared logging.yaml (from service-base)
  • ✓ Service-specific auth.yaml (from auth-service)

Step 8: Create More Services

Create payment-service and notification-service the same way:

# Payment service
uv init && uv add service-base && uv run pyrig init

# Notification service
uv init && uv add service-base && uv run pyrig init

All three services now have:

  • ✓ Same logging configuration
  • ✓ Same documentation theme
  • ✓ Same dependencies (including cryptography)
  • ✓ Same linting rules
  • ✓ Same coverage threshold
  • ✓ Automatic healing via autouse fixtures

How It Works: The Discovery Mechanism

pyrig uses a sophisticated discovery system to find and initialize configurations across package dependencies. For complete technical details, see:

Quick Overview

ConfigFile Discovery:

  1. Build dependency graph: pyrig → service-base → auth-service
  2. Find all <package>.rig.configs modules
  3. Discover all ConfigFile subclasses
  4. Keep only leaf classes (classes with no child subclasses in the result set)
  5. Validate all leaf classes

Autouse Fixture Healing:

  1. tests/conftest.py activates pyrig's test plugins
  2. Discover fixtures from all packages in dependency chain
  3. assert_root_is_correct runs on every test session
  4. Validates all ConfigFiles are correct
  5. Raises descriptive error if validation fails (prompting you to review the changes from ConfigFile.validate_subclasses that the autouse fixture did to fix the issue)

Propagation Flow

When you update the base package, changes automatically propagate to all dependent services:

  1. Update service-base - Make changes to config files
  2. Release new version - GitHub Actions automatically creates release
  3. Services update dependency - Run uv add service-base --upgrade
  4. Run pytest or pyrig mkroot - Triggers validation
  5. assert_root_is_correct runs - Autouse fixture validates all configs
  6. If incorrect - ConfigFile.validate_subclasses() validates all incorrect ConfigFiles
  7. Files created/updated - Missing configs added, incorrect values fixed
  8. Tests continue - Or fail with descriptive error showing what changed

See Autouse Fixtures for details on the validation system.

Real-World Benefits

Scenario: Security Audit Requires New Tool Dependency

Problem: Security audit requires all services use safety alongside the default security checker.

Solution (5 minutes):

  1. Override dev_dependencies() on your tool in service-base:
def dev_dependencies(self) -> list[str]:
    return [*super().dev_dependencies(), "safety"]
  1. Commit and push: GitHub Actions releases service-base v1.2.0

  2. In each service:

# all you gotta do
uv lock --upgrade && uv sync
uv run pytest  # Auto-heals to add safety or do pyrig mkroot
# commit and push and the new release etc happens automatically

Result: All your microservices now have safety without manual edits.

Scenario: Change Documentation Theme

Problem: Rebrand requires new colors and logo.

Solution (2 minutes):

  1. Update service-base/rig/configs/docs/mkdocs.py: Change colors, then do the same steps as above
  2. Release new version
  3. Services update → All docs sites automatically rebranded at GitHub Pages

Scenario: New Microservice

Problem: Need to create inventory-service with all shared standards.

Solution (30 seconds):

uv init && uv add service-base && uv run pyrig init

Result: Production-ready service with all standards in 30 seconds.

Key Takeaways

  1. One source of truth: service-base defines all standards
  2. Automatic propagation: Changes flow to all services via dependency updates
  3. Self-healing: Autouse fixtures ensure compliance on every test run
  4. Override flexibility: Services can customize while keeping standards by subclassing with the same class name
  5. Leaf class pattern: Subclassing with the same name automatically overrides parent configs (pyrig keeps only the most-derived class)
  6. Discovery mechanism: Dependency graph + module discovery enables multi-package architecture
  7. Zero manual sync: No need to manually update 50+ services

Advanced: Multi-Level Inheritance

You can create deeper hierarchies:

pyrig
  └── service-base (shared standards)
      ├── backend-base (backend-specific: databases, APIs)
      │   ├── auth-service
      │   └── payment-service
      └── ml-base (ML-specific: models, training)
          ├── recommendation-service
          └── fraud-detection-service

Each level adds/overrides configs, and leaf services inherit the entire chain.

Testing the Setup

Verify everything works if you want:

In service-base:

# Verify config discovery
uv run python -c "
from pyrig.rig.configs.base.base import ConfigFile
configs = ConfigFile.subclasses()
print(f'Found {len(configs)} config files')
for c in configs:
    print(f'  - {c.__module__}.{c.__name__}')
"

# Run tests to verify autouse fixtures work
uv run pytest -v

In auth-service:

# Verify inherited configs
cat mkdocs.yml  # Should show custom theme
cat pyproject.toml  # Should show shared dependencies

# Verify auto-healing
rm mkdocs.yml  # Delete inherited config
uv run pytest  # Auto-recreates it!
cat mkdocs.yml  # Should show custom theme

Summary

pyrig's multi-package architecture enables:

  1. Centralized standards in base packages
  2. Automatic discovery via dependency graph traversal
  3. Override mechanism via subclassing with the same class name (leaf classes automatically override parents)
  4. Self-healing via autouse fixtures on every test run
  5. Zero manual sync across unlimited services

This pattern scales from 2 services to infinite+ services with the same simplicity.

Note: If you somehow end up creating a structure over 20 dependencies deep in a dependency chain, the health check cron will get confused as the day has only 24 hours and it staggers it per hour. See more at: Health Check Documentation