Getting Started¶
Build a minimal waku app, then extend it into a multi-module project with configuration and multiple services.
Why modules?¶
A typical Python service — every dependency is a hardcoded import:
from db import get_session
from config import settings
from notifications import send_email
class UserService:
def create_user(self, name: str) -> User:
session = get_session() # (1)!
user = User(name=name)
session.add(user)
session.commit()
if settings.SEND_WELCOME_EMAIL: # (2)!
send_email(user.email, '...') # (3)!
return user
- Direct import — how do you test this without a real database?
- Global config access — how do you swap settings per environment?
- Hidden cross-module dependency — nothing prevents
notificationsfrom importingUserServiceback.
Same functionality — dependencies are injected, boundaries are explicit:
class UserService:
def __init__(self, session: AsyncSession, notifier: INotifier) -> None:
self.session = session
self.notifier = notifier
async def create_user(self, name: str) -> User:
user = User(name=name)
self.session.add(user)
await self.notifier.notify(user.email, '...')
return user
@module(
providers=[scoped(UserService)], # (1)!
imports=[DatabaseModule, NotificationModule], # (2)!
exports=[UserService], # (3)!
)
class UserModule:
pass
- Providers are declared, not imported — swap the DB by changing one provider.
- Module imports make dependencies explicit — circular dependencies are caught at startup by validation.
- Exports control what other modules can access — the module's public API.
Creating Your First waku Application¶
Step 1: Create the Basic Structure¶
Create a new directory for your project and set up your files:
Step 2: Define Your Services¶
Define a service in services.py:
| services.py | |
|---|---|
Step 3: Create Modules¶
Define the modules and bootstrap the app in app.py:
providersdefines which providers this module creates and manages.scopedcreates a new instance for each container context entrance.exportsmakes providers available to other modules that import this one. Without an export, a provider is only injectable within its own module.WakuFactoryis the composition root — define your module tree once, reuse it across API server, CLI, and workers.- This is for standalone scripts and demos. In real applications, your framework handles container scoping — see the FastAPI tab.
- Manages waku lifecycle (extension hooks, startup/shutdown) through FastAPI's lifespan. Dishka handles per-request container scoping automatically.
setup_dishkaconnects the DI container to FastAPI — dependencies resolve automatically per request.@injectfrom Dishka's FastAPI integration enables automatic dependency resolution for this handler.Injected[Type]marks a parameter for injection. See Framework Integrations for other frameworks.
Step 4: Run Your Application¶
Run the application with:
You should see the output:
Creating a More Realistic Application¶
Now add configuration, multiple modules, and cross-module dependencies.
Step 1: Set Up the Project Structure¶
Create a more complete project structure:
app/
├── __init__.py
├── __main__.py
├── application.py
├── modules/
│ ├── __init__.py
│ ├── greetings/
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── services.py
│ │ └── module.py
│ └── users/
│ ├── __init__.py
│ ├── models.py
│ ├── services.py
│ └── module.py
└── settings.py
Step 2: Add Configuration Module¶
Define settings and a configuration module:
Tip
Consider using pydantic-settings or similar libraries for settings management in production applications.
is_global=True makes this module's exports available to every module in the app
without explicit imports. Without it, every module that needs settings would have to
add imports=[ConfigModule].
Learn more: Global Modules.
Step 3: Create Modules¶
Greeting Module¶
| app/modules/greetings/models.py | |
|---|---|
| app/modules/greetings/module.py | |
|---|---|
User Module¶
| app/modules/users/models.py | |
|---|---|
| app/modules/users/module.py | |
|---|---|
Step 4: Create the Application Module¶
Define the root module and bootstrap function:
ConfigModule.register(env='dev') is a dynamic module —
it lets you pass parameters at import time, so the module controls how the value becomes a provider.
The other modules are imported directly — they don't need parameters.
Tip
The bootstrap function is the composition root — one place to wire the entire module tree. Every entrypoint (API, CLI, worker) calls it.
Step 5: Create the Main Entrypoint¶
Tip
In production, you would use FastAPI, Litestar, or another framework
instead of a standalone script. The double context manager (async with app, app.container())
disappears — your framework's integration handles container scoping per request.
Step 6: Run Your Application¶
Expected output:
Next steps¶
- Understand the Module System in depth
- Explore Dependency Injection patterns
- Integrate with FastAPI, Litestar, or other frameworks
- Add Extensions for lifecycle hooks
- Use the Mediator (CQRS) for command/query separation
- Build event-driven systems with Event Sourcing
Further reading¶
- The Software Architecture Chronicles by Herberto Graça distills all popular software architectural styles into a single approach — a great resource for understanding the principles behind waku.