Architecture PatternsΒΆ
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.
Related pagesΒΆ
What's Next?ΒΆ
Continue to Dependency Injection to formalize service wiring and keep architecture boundaries clean.