pymediate
Advanced

Best practices

An index of topic-specific best practices, plus cross-cutting advice on project structure, DI choice, and request versioning.

Each guide page ends with a best-practices section scoped to its own topic. This page indexes those, and adds a few cross-cutting practices that don't belong to any single topic.

Topic-specific best practices

  • Requests and responses — keep requests simple, type every field, organize by feature.
  • Handlers — single responsibility, dependency injection, stateless by default.
  • The mediator — one mediator instance per application, don't mix dispatch styles.
  • Pipeline behaviors — keep behaviors focused, mind registration order, short-circuit only intentionally.
  • Dataclassesfrozen=True for requests, default_factory for mutable defaults, validate in __post_init__.
  • Error handling — keep domain errors independent of the framework; map them at the edge.

Project structure

Organize by business capability, not by technical layer — keep a feature's requests, handlers, and tests together rather than splitting them into parallel requests/, handlers/, tests/ trees:

app/
    orders/
        requests.py
        handlers.py
        test_handlers.py
    users/
        requests.py
        handlers.py
        test_handlers.py
    payments/
        requests.py
        handlers.py
        test_handlers.py

This keeps each handler's test file next to the handler it covers, per testing: fixtures and organization.

Choosing between Services and a DI container

Both implement the same ServiceProvider protocol, so Mediator doesn't care which you use — the choice is about how you want to manage object lifetimes and wiring, not about PyMediate itself:

  • Services is enough for most applications: a few lines of explicit registration with no framework to learn. Reach for it first.
  • DependencyInjectorServiceProvider earns its keep once you have real lifetime management to do — Singleton vs. Factory vs. scoped providers, or dependencies that themselves need building from configuration. See dependency injection.

Don't reach for a DI container just to avoid writing services.add(...) calls — that's not the problem it solves.

Versioning requests

Once a request type ships, treat its fields as a public contract. Add optional fields with defaults rather than changing existing ones; if a change would break existing callers, introduce a new versioned request type and translate at the adapter boundary:

@dataclass
class CreateUserRequestV1(Request[UserCreated]):
    username: str
    email: str

@dataclass
class CreateUserRequestV2(Request[UserCreated]):
    username: str
    email: str
    phone: str | None = None

def translate_v1_to_v2(v1: CreateUserRequestV1) -> CreateUserRequestV2:
    return CreateUserRequestV2(username=v1.username, email=v1.email, phone=None)

This keeps version translation in the adapter, not the handler — the handler only ever sees one shape.

Next steps

  • Type safety — what mypy checks statically vs. what PyMediate checks at import time
  • Testing — testing handlers, the mediator, and pipeline behaviors
  • Troubleshooting — common configuration mistakes and fixes

On this page