Skip to content

Service Providers

Introduction

Service providers are the central place of all Arvel application bootstrapping. Your own application, as well as all of Arvel's core services, are bootstrapped via service providers.

But what do we mean by "bootstrapped"? In general, we mean registering things: registering container bindings, event listeners, middleware, and routes. Service providers are the central place to configure your application.

If you open bootstrap/providers.py, you'll see your application's providers list. Each provider gives you a place to wire one slice of your application together.

Writing Service Providers

All service providers extend the ServiceProvider class. Most contain a register and a boot method. Generate a new provider with the make:provider command:

arvel make:provider AppServiceProvider

A provider receives the Application in its constructor and exposes it as self.app, with the root container as self.container:

from arvel.providers import ServiceProvider


class AppServiceProvider(ServiceProvider):
    def register(self) -> None:
        self.container.singleton(InventoryService)

    async def boot(self) -> None:
        ...

    async def shutdown(self) -> None:
        ...

The Register Method

Within the register method, you should only bind things into the container. The register method is synchronous and runs during the application's build step, before any other provider has booted. You should never attempt to register event listeners, do I/O, resolve another provider's service, or perform any other side effect within the register method — those services may not be bound yet.

def register(self) -> None:
    self.container.singleton(PaymentGateway, StripeGateway)

The Boot Method

The boot method is asynchronous and is called after all other service providers have been registered, meaning you have access to every binding the framework and your app have registered. This is the place for wiring that depends on other services — attaching listeners, registering broadcast channels, opening connections:

from arvel.events.dispatcher import EventDispatcher


async def boot(self) -> None:
    # OrderPlaced is your event; SendOrderConfirmation your listener.
    dispatcher = self.container.make(EventDispatcher)
    dispatcher.listen(OrderPlaced, SendOrderConfirmation)

The Shutdown Method

The shutdown method runs on application teardown, in reverse registration order. Release any resources your provider opened:

async def shutdown(self) -> None:
    await self._connection.close()

Provider Members

Member Purpose
self.app The Application.
self.container The root container.
register() Sync. Register container bindings only.
boot() Async. Startup wiring after all providers register.
shutdown() Async. Teardown, reverse order.
commands() Return CLI command classes/instances this provider contributes.
publishes(paths, *, tag=, is_migrations=) Declare files an app can copy out with vendor:publish.
safe_config(cls, *, default) Resolve a config object, falling back to default if it isn't available.

See Request Lifecycle for the full register-vs-boot ordering.

Registering Providers

All service providers are registered in the bootstrap/providers.py file. This file exposes a providers list:

from arvel.providers import ServiceProvider
from app.providers.app_service_provider import AppServiceProvider

providers: list[type[ServiceProvider]] = [
    AppServiceProvider,
]

Your providers run after the framework's baseline providers, so framework services are already bound by the time your register() runs.

Contributing CLI Commands

A provider exposes console commands by returning them from commands(). The framework collects them from every provider when the app boots:

class AppServiceProvider(ServiceProvider):
    def commands(self) -> list[type]:
        return [SyncInventoryCommand]

You may return command classes (instantiated with no arguments) or pre-built command instances (useful when a command needs injected dependencies). See CLI Commands.

Publishing Assets

Package authors declare files an application can copy into its own tree. Call publishes from boot, mapping source paths to destinations:

from pathlib import Path


async def boot(self) -> None:
    package_root = Path(__file__).parent
    self.publishes(
        {package_root / "migrations": "database/migrations"},
        tag="my-package",
        is_migrations=True,
    )

There's no self.package_path — resolve your package directory yourself (here, from the provider module's own __file__).

Applications then copy them out:

arvel vendor:publish --tag=my-package

Framework Providers Reference

Auto-Registered Providers

These baseline providers register automatically — you don't list them, but knowing they exist explains where framework bindings come from:

Provider Binds / does
ConfigServiceProvider Config accessor; one singleton per registered ArvelSettings.
LogServiceProvider No-op placeholder; the OpenTelemetry-backed Log facade is bootstrapped by ObservabilityServiceProvider.
LangServiceProvider Translator for localization.
ContextServiceProvider Per-request context store (the Context facade).
ObservabilityServiceProvider Tracing, logging, metrics.
DatabaseServiceProvider Async engine, session maker, AsyncSession, Schema.
HttpServiceProvider Router, exception handler, rate-limiter store, maintenance manager.
SchedulerServiceProvider Schedule, scheduler kernel; discovers app/console/kernel.py.
ConsoleServiceProvider Collects every provider's commands() (always registered last).

Opt-In Providers

Many subsystems are not auto-registered. Add their provider to bootstrap/providers.py to use them, or their facade raises FacadeNotBoundError (or a RuntimeError):

Provider Import Enables
CacheServiceProvider arvel.providers.cache_provider Cache
SessionServiceProvider arvel.providers.session_provider Session
StorageServiceProvider arvel.providers.storage_provider Storage
BroadcastServiceProvider arvel.providers.broadcast_provider Broadcast
AuthServiceProvider arvel.auth.provider Auth, Gate, auth routes
MailServiceProvider arvel.mail.providers.mail_service_provider Mail
QueueServiceProvider arvel.queue.providers.queue_service_provider Bus, queue commands
EventServiceProvider arvel.events.providers.event_service_provider Event
NotificationServiceProvider arvel.notifications.providers.notification_service_provider Notification
from arvel.providers.cache_provider import CacheServiceProvider
from arvel.providers.storage_provider import StorageServiceProvider

providers: list[type[ServiceProvider]] = [
    CacheServiceProvider,
    StorageServiceProvider,
    AppServiceProvider,
]

Note

Companion packages (arvel-oauth, arvel-permission, …) are not auto-registered either. Add their provider to bootstrap/providers.py yourself. See the packages overview.

Provider Ordering

The framework resolves the provider chain in a fixed order: the baseline head providers (config, logging, language, context, observability, database, HTTP, scheduler) run first, then your providers in the order you list them, and finally the ConsoleServiceProvider is forced last so it can collect commands from everything else. Duplicate entries are de-duplicated, so re-listing a baseline provider is a harmless no-op.