Перейти к содержанию

Architecture Patterns

Warning

The current page still doesn't have a translation for this language.

But you can help translating it: Contributing.

When Ravyn projects grow, architecture decisions start to matter more than syntax. This guide shows practical patterns and when to choose each one.


A simple decision map

Small API, single team, fast iteration
  -> Monolith with feature folders

Growing API, multiple domains, independent ownership
  -> Modular / feature-based with Includes and Routers

Complex business rules and long-lived domains
  -> Layered or DDD-inspired structure

Independent deploy/release cycles per domain
  -> Microservices (HTTP/gRPC between services)

Baseline structure for most projects

For many production Ravyn apps, this is a strong default:

app/
  main.py
  settings.py
  users/
    routes.py
    service.py
    schemas.py
  billing/
    routes.py
    service.py
    schemas.py

Use Include in main.py to keep composition explicit.

from ravyn import Include, Ravyn


app = Ravyn(
    routes=[
        Include("/users", namespace="app.users.routes"),
        Include("/billing", namespace="app.billing.routes"),
    ]
)

Pattern 1: Monolith (clean and simple)

All code ships as one service.

When it fits

  • Team is small.
  • Domains are tightly coupled.
  • Operational simplicity matters most.

Example

from ravyn import Ravyn, get


@get("/")
def home() -> dict:
    return {"message": "Welcome"}


app = Ravyn(routes=[home])

Pattern 2: Modular feature-based architecture

Split code by domain (users, orders, payments).

Why this is often the best middle ground

  • Better ownership by feature.
  • Lower merge conflicts.
  • Easier onboarding for new developers.

Example module

# app/users/routes.py
from ravyn import get


@get("/")
def list_users() -> dict:
    return {"users": ["Alice", "Bob"]}
# app/main.py
from ravyn import Include, Ravyn


app = Ravyn(routes=[Include("/users", namespace="app.users.routes")])

Pattern 3: Layered / DDD-inspired approach

Separate transport, business, and persistence concerns.

HTTP layer (routes/controllers)
    -> Application/service layer
        -> Repository/DAO layer
            -> Database or external systems

Ravyn mapping

  • HTTP layer: Gateway, Router, Controller
  • Service/repository wiring: Inject, Injects, Factory
  • Policies: permissions, middleware, interceptors

Example

from ravyn import Inject, Injects, Ravyn, get


class UserService:
    async def list(self) -> list[str]:
        return ["Alice", "Bob"]


def get_user_service() -> UserService:
    return UserService()


@get("/users", dependencies={"service": Inject(get_user_service)})
async def list_users(service: UserService = Injects()) -> dict:
    return {"users": await service.list()}


app = Ravyn(routes=[list_users])

Pattern 4: Microservices

Split domains into independently deployable services.

Use this when

  • Teams release independently.
  • Different scalability profiles are required.
  • Service boundaries are stable and intentional.

Ravyn supports this model with normal HTTP boundaries and experimental gRPC integration.


Common pitfalls

1. Premature microservices

Start modular inside one service first. Extract later when boundaries are proven.

2. Domain logic in handlers

Keep handlers thin; move business rules into services/DAOs.

3. Inconsistent route composition

Use a single composition style (Include by namespace or explicit route lists) to keep route trees predictable.


What's Next?

Continue to Dependency Injection to formalize service wiring and keep architecture boundaries clean.