Skip to content

Overview

waku logo

枠 · framework for Python backends that grow


PyPI Python version Downloads CI/CD codecov GitHub license Ask DeepWiki


Python makes it easy to build a backend. waku makes it easy to keep growing one.

As your project scales, problems creep in: services import each other freely, swapping a database means editing dozens of files, and nobody can tell which module depends on what. Python has no built-in way to enforce component boundaries — so what starts as clean code quietly becomes a tangle of implicit dependencies that discipline alone can't prevent.

waku gives you modules with explicit boundaries, type-safe DI powered by Dishka, and integrated CQRS and event sourcing — so your codebase stays manageable as it scales.

Installation

uv add waku
pip install waku

Structure that scales

  • Package by Component


    Each module is a self-contained unit with its own providers. Explicit imports and exports control what crosses boundaries — validated at startup, not discovered in production.

  • Dependency Inversion


    Define interfaces in your application core, bind adapters in infrastructure modules. Swap a database, a cache, or an API client by changing one provider — powered by Dishka.

  • One Core, Any Entrypoint


    Build your module tree once with WakuFactory. Plug it into FastAPI, Litestar, FastStream, Aiogram, CLI, or workers — same logic everywhere.

Built-in capabilities

  • CQRS & Mediator


    DI alone doesn't decouple components — you need events. The mediator dispatches commands, queries, and events so components never reference each other directly. Pipeline behaviors handle cross-cutting concerns.

  • Event Sourcing


    Full event sourcing support — aggregates, projections, snapshots, upcasting, and the decider pattern with built-in SQLAlchemy adapters.

  • Lifecycle & Extensions


    Hook into startup, shutdown, and module initialization with extensions. Add validation, logging, or custom behaviors — decoupled from your business logic.

How it works

Group related providers into modules with explicit imports and exports. WakuFactory wires the module tree into a DI container. Plug it into your framework — FastAPI, Litestar, or anything else — and you're done.

Recommended project structure

Following Explicit Architecture and its code reflection, only UI and shared infrastructure live at the top level — each feature component is a vertical slice with its own domain, application, and infrastructure layers, wired together by a waku module:

your_app/
├── core/
│   ├── components/
│   │   ├── users/              # feature component
│   │   │   ├── domain/         # entities, value objects, events
│   │   │   ├── application/    # use cases, handlers, ports
│   │   │   ├── infra/          # repositories, adapters
│   │   │   └── module.py       # waku module
│   │   └── orders/
│   │       ├── domain/
│   │       ├── application/
│   │       ├── infra/
│   │       └── module.py
│   ├── ports/                  # shared system ports
│   └── shared_kernel/          # cross-component contracts
├── infra/                      # cross-cutting infrastructure
│   └── module.py
├── ui/                         # API routes, CLI handlers
└── app.py                      # composition root

Quick example

A service, a module, and a container — the minimal waku app:

app.py
import asyncio

from waku import WakuFactory, module
from waku.di import scoped


class GreetingService:
    async def greet(self, name: str) -> str:
        return f'Hello, {name}!'


@module(providers=[scoped(GreetingService)])
class GreetingModule:
    pass


@module(imports=[GreetingModule])
class AppModule:
    pass


async def main() -> None:
    app = WakuFactory(AppModule).create()

    async with app, app.container() as c:
        svc = await c.get(GreetingService)
        print(await svc.greet('waku'))


if __name__ == '__main__':
    asyncio.run(main())

Modules control visibility. InfrastructureModule exports ILoggerUserModule imports it. Dependencies are explicit, not implicit.

app.py
import asyncio
from typing import Protocol

from waku import WakuFactory, module
from waku.di import scoped, singleton


class ILogger(Protocol):
    async def log(self, message: str) -> None: ...


class ConsoleLogger(ILogger):
    async def log(self, message: str) -> None:
        print(f'[LOG] {message}')


class UserService:
    def __init__(self, logger: ILogger) -> None:
        self.logger = logger

    async def create_user(self, username: str) -> str:
        user_id = f'user_{username}'
        await self.logger.log(f'Created user: {username}')
        return user_id


@module(
    providers=[singleton(ILogger, ConsoleLogger)],
    exports=[ILogger],
)
class InfrastructureModule:
    pass


@module(
    imports=[InfrastructureModule],
    providers=[scoped(UserService)],
)
class UserModule:
    pass


@module(imports=[UserModule])
class AppModule:
    pass


async def main() -> None:
    app = WakuFactory(AppModule).create()

    async with app, app.container() as c:
        user_service = await c.get(UserService)
        user_id = await user_service.create_user('alice')
        print(f'Created user with ID: {user_id}')


if __name__ == '__main__':
    asyncio.run(main())

Next steps

  • Getting Started


    Install waku, build a modular app, and connect it to your framework

  • Examples


    Working projects showing real usage patterns with FastAPI, Litestar, and more

  • API Reference


    Full module, class, and function reference