Phestus v0.1 is just weeks away from being released! Explore the latest modules and documentation.Learn more

Providers & Modules

Providers & Modules

Modules and providers are two separate parts of the Phestus architecture.

A module defines a capability.

A provider implements that capability.

This distinction allows Phestus to remain modular without forcing every module to depend on a particular technology.

The Basic Relationship

The relationship can be represented as:

Module
  │
  │ defines
  ▼
Capability
  │
  │ implemented by
  ▼
Provider
  │
  │ uses
  ▼
Technology

For example:

Queue Module
     │
     ▼
QueueProvider
     │
     ▼
BullMQQueueProvider
     │
     ▼
BullMQ

The Queue Module defines what a queue system can do.

The BullMQ provider determines how those operations are performed.

Why Not Put the Implementation in the Module?

It would be possible to build the Queue Module directly around BullMQ.

For example:

Queue Module
     │
     └── BullMQ

However, this means the module is coupled to BullMQ.

Replacing BullMQ would require changing the module itself.

The provider model separates those concerns:

Queue Module
     │
     ├── BullMQ Provider
     ├── SQS Provider
     └── Custom Provider

The Queue Module remains focused on the queue capability.

Each provider is responsible for translating that capability into a particular implementation.

Modules Define Interfaces

A module can define a provider interface.

For example:

export interface QueueProvider {
    enqueue<T>(
        queueName: string,
        payload: T,
    ): Promise<void>;

    consume<T>(
        queueName: string,
        handler: (
            message: QueueMessage<T>,
        ) => Promise<void>,
        options?: QueueConsumeOptions,
    ): Promise<() => Promise<void>>;
}

The interface defines the operations required from a queue implementation.

The module does not need to know how those operations are implemented.

Providers Implement Interfaces

A provider implements the interface:

export class BullMQQueueProvider
    implements QueueProvider
{
    // ...
}

The implementation can then use BullMQ:

const queue =
    new Queue(
        queueName,
        {
            connection: this.connection,
            defaultJobOptions:
                this.defaultJobOptions,
        },
    );

BullMQ-specific code remains inside the provider.

Provider Metadata Connects the Two

The provider identifies its module through moduleSlug:

moduleSlug = "queue";

The provider itself has a unique slug:

slug = "bullmq";

Together, these establish:

Provider: bullmq
Module:   queue

Or conceptually:

bullmq implements queue

This metadata allows the Phestus runtime to understand which capability a provider belongs to.

Multiple Providers

A module can have multiple providers.

For example:

              Queue Module
                   │
        ┌──────────┼──────────┐
        │          │          │
        ▼          ▼          ▼
     BullMQ       SQS       Custom

Each provider implements the same capability.

This allows applications to choose an implementation without changing the module's API.

The module remains stable while provider implementations can evolve independently.

Provider Selection

Providers are registered with the Phestus runtime.

For example:

const phestus = new Phestus({
    service,
    logger,
    eventBus,

    plugins: [
        queuePlugin,
    ],
});

The plugin can supply the provider:

const queuePlugin: PhestusPlugin = {
    manifest: {
        slug: "queue-plugin",
        name: "Queue Plugin",
        version: "0.1.0",
    },

    providers: [
        new BullMQQueueProvider({
            connection,
        }),
    ],
};

Phestus registers the provider:

this.providers.register(
    provider,
);

The runtime can then retrieve it:

phestus.getProvider(
    "bullmq",
);

The important point is that the application does not need to construct the provider while using the module.

The provider becomes part of the configured Phestus runtime.

Plugins Package Providers

Providers are commonly distributed through plugins.

This gives Phestus three distinct architectural responsibilities:

Module
  │
  └── defines capability

Provider
  │
  └── implements capability

Plugin
  │
  └── packages and distributes
      modules and providers

For example:

Queue Plugin
     │
     ├── Queue Module
     │
     └── BullMQ Provider

The plugin provides the pieces required to add that capability to an application.

A Complete Example

Consider a queue implementation.

Module

The Queue Module defines the capability:

export interface QueueProvider {
    enqueue<T>(
        queueName: string,
        payload: T,
    ): Promise<void>;

    consume<T>(
        queueName: string,
        handler: (
            message: QueueMessage<T>,
        ) => Promise<void>,
    ): Promise<() => Promise<void>>;
}

Provider

The BullMQ provider implements it:

export class BullMQQueueProvider
    implements QueueProvider
{
    slug = "bullmq";
    name = "BullMQ Queue Provider";
    moduleSlug = "queue";
    version = "0.1.0";

    // ...
}

Plugin

The plugin packages the provider:

const queuePlugin: PhestusPlugin = {
    manifest: {
        slug: "queue-plugin",
        name: "Queue Plugin",
        version: "0.1.0",
    },

    providers: [
        new BullMQQueueProvider({
            connection,
        }),
    ],
};

Application

The application registers the plugin:

const phestus =
    new Phestus({
        service,
        logger,
        eventBus,

        plugins: [
            queuePlugin,
        ],
    });

The final architecture becomes:

Application
     │
     ▼
  Phestus
     │
     ▼
Queue Plugin
     │
     ├── Queue Module
     │       │
     │       └── Queue capability
     │
     └── BullMQ Provider
             │
             └── BullMQ

The Separation of Responsibilities

The separation can be summarized simply.

Modules

Modules answer:

What can the application do?

They define capabilities and the interfaces through which those capabilities are consumed.

Providers

Providers answer:

How is that capability implemented?

They contain technology-specific implementation details.

Plugins

Plugins answer:

How are these pieces packaged and introduced into an application?

They can distribute modules and providers together and declare the dependencies required by those components.

Building Against Capabilities

The provider architecture encourages application code to depend on capabilities rather than technologies.

Instead of designing an application around:

BullMQ
Redis
Stripe
PostgreSQL

the application can work with:

Queue
Payments
Storage
Data

The underlying implementations remain replaceable.

This is the purpose of the provider abstraction in Phestus:

Application
     │
     ▼
Capability
     │
     ▼
Provider
     │
     ▼
Implementation

The application knows what it needs to accomplish.

The provider determines how the underlying technology accomplishes it.