---
title: 'Custom Strategies in Plugins'
---

When building Vendure plugins, you often need to provide extensible, pluggable implementations for specific features. The **strategy pattern** is the perfect tool for this, allowing plugin users to customize behavior by providing their own implementations.

This guide shows you how to implement custom strategies in your plugins, following Vendure's established patterns and best practices.

## Overview

A strategy in Vendure is a way to provide a pluggable implementation of a particular feature. Custom strategies in plugins allow users to:

- Override default behavior with their own implementations
- Inject dependencies and services through the `init()` lifecycle method
- Clean up resources using the `destroy()` lifecycle method
- Configure the strategy through the plugin's init options

## Creating a Strategy Interface

First, define the interface that your strategy must implement. All strategy interfaces should extend `InjectableStrategy` to support dependency injection and lifecycle methods.

```ts title="src/strategies/my-custom-strategy.ts"
import { InjectableStrategy, RequestContext } from '@vendure/core';

export interface MyCustomStrategy extends InjectableStrategy {
    /**
     * Process some data and return a result
     */
    processData(ctx: RequestContext, data: any): Promise<string>;

    /**
     * Validate the input data
     */
    validateInput(data: any): boolean;
}
```

## Implementing a Default Strategy

Create a default implementation that users can extend or replace:

```ts title="src/strategies/default-my-custom-strategy.ts"
import { Injector, RequestContext, Logger } from '@vendure/core';
import { MyCustomStrategy } from './my-custom-strategy';
import { SomeOtherService } from '../services/some-other.service';
import { loggerCtx } from '../constants';

export class DefaultMyCustomStrategy implements MyCustomStrategy {
    private someOtherService: SomeOtherService;

    async init(injector: Injector): Promise<void> {
        // Inject dependencies during the init phase
        this.someOtherService = injector.get(SomeOtherService);

        // Perform any setup logic
        Logger.info('DefaultMyCustomStrategy initialized', loggerCtx);
    }

    async destroy(): Promise<void> {
        // Clean up resources if needed
        Logger.info('DefaultMyCustomStrategy destroyed', loggerCtx);
    }

    async processData(ctx: RequestContext, data: any): Promise<string> {
        // Validate input first
        if (!this.validateInput(data)) {
            throw new Error('Invalid input data');
        }

        // Use injected service to process data
        const result = await this.someOtherService.doSomething(ctx, data);
        // ... do something with the result
        return result;
    }

    validateInput(data: any): boolean {
        return data != null && typeof data === 'object';
    }
}
```

## Adding Strategy to Plugin Options

Define your plugin's initialization options to include the strategy:

```ts title="src/types.ts"
import { MyCustomStrategy } from './strategies/my-custom-strategy';

export interface MyPluginInitOptions {
    /**
     * Custom strategy for processing data
     * @default DefaultMyCustomStrategy
     */
    processingStrategy?: MyCustomStrategy;

    /**
     * Other plugin options
     */
    someOtherOption?: string;
}
```

## Configuring the Plugin

In your plugin definition, provide the default strategy and allow users to override it:

```ts title="src/my-plugin.ts"
import { PluginCommonModule, VendurePlugin, Injector } from '@vendure/core';
import { OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';

import { MY_PLUGIN_OPTIONS } from './constants';
import { MyPluginInitOptions } from './types';
import { DefaultMyCustomStrategy } from './strategies/default-my-custom-strategy';
import { MyPluginService } from './services/my-plugin.service';
import { SomeOtherService } from './services/some-other.service';

@VendurePlugin({
    imports: [PluginCommonModule],
    providers: [
        MyPluginService,
        SomeOtherService,
        {
            provide: MY_PLUGIN_OPTIONS,
            useFactory: () => MyPlugin.options,
        },
    ],
    configuration: config => {
        // You can also configure core Vendure strategies here if needed
        return config;
    },
    compatibility: '^3.0.0',
})
export class MyPlugin implements OnApplicationBootstrap, OnApplicationShutdown {
    static options: MyPluginInitOptions;

    constructor(private moduleRef: ModuleRef) {}

    static init(options: MyPluginInitOptions) {
        this.options = {
            // Provide default strategy if none specified
            processingStrategy: new DefaultMyCustomStrategy(),
            ...options,
        };
        return MyPlugin;
    }

    async onApplicationBootstrap() {
        await this.initStrategy();
    }

    async onApplicationShutdown() {
        await this.destroyStrategy();
    }

    private async initStrategy() {
        const strategy = MyPlugin.options.processingStrategy;
        if (strategy && typeof strategy.init === 'function') {
            const injector = new Injector(this.moduleRef);
            await strategy.init(injector);
        }
    }

    private async destroyStrategy() {
        const strategy = MyPlugin.options.processingStrategy;
        if (strategy && typeof strategy.destroy === 'function') {
            await strategy.destroy();
        }
    }
}
```

## Using the Strategy in Services

Access the strategy through dependency injection in your services:

```ts title="src/services/my-plugin.service.ts"
import { Injectable, Inject } from '@nestjs/common';
import { RequestContext } from '@vendure/core';

import { MY_PLUGIN_OPTIONS } from '../constants';
import { MyPluginInitOptions } from '../types';

@Injectable()
export class MyPluginService {
    constructor(@Inject(MY_PLUGIN_OPTIONS) private options: MyPluginInitOptions) {}

    async processUserData(ctx: RequestContext, userData: any): Promise<string> {
        // Delegate to the configured strategy
        return this.options.processingStrategy.processData(ctx, userData);
    }

    validateUserInput(data: any): boolean {
        return this.options.processingStrategy.validateInput(data);
    }
}
```

## User Implementation Example

Plugin users can now provide their own strategy implementations:

```ts title="src/my-custom-implementation.ts"
import { Injector, RequestContext, Logger } from '@vendure/core';
import { MyCustomStrategy } from '@my-org/my-plugin';
import { ExternalApiService } from './external-api.service';
import { loggerCtx } from '../constants';

export class CustomProcessingStrategy implements MyCustomStrategy {
    private externalApi: ExternalApiService;

    async init(injector: Injector): Promise<void> {
        this.externalApi = injector.get(ExternalApiService);

        // Initialize external API connection
        await this.externalApi.connect();
        Logger.info('Custom processing strategy initialized', loggerCtx);
    }

    async destroy(): Promise<void> {
        // Clean up external connections
        if (this.externalApi) {
            await this.externalApi.disconnect();
        }
        Logger.info('Custom processing strategy destroyed', loggerCtx);
    }

    async processData(ctx: RequestContext, data: any): Promise<string> {
        if (!this.validateInput(data)) {
            throw new Error('Invalid data format');
        }

        // Use external API for processing
        const result = await this.externalApi.processData(data);
        return `Processed: ${result}`;
    }

    validateInput(data: any): boolean {
        // Custom validation logic
        return data && data.type === 'custom' && data.value;
    }
}
```

## Plugin Configuration by Users

Users configure the plugin with their custom strategy:

```ts title="vendure-config.ts"
import { VendureConfig } from '@vendure/core';
import { MyPlugin } from '@my-org/my-plugin';
import { CustomProcessingStrategy } from './my-custom-implementation';

export const config: VendureConfig = {
    plugins: [
        MyPlugin.init({
            processingStrategy: new CustomProcessingStrategy(),
            someOtherOption: 'custom-value',
        }),
    ],
    // ... other config
};
```

## Strategy with Options

You can also create strategies that accept configuration options:

```ts title="src/strategies/configurable-strategy.ts"
import { Injector, RequestContext } from '@vendure/core';
import { MyCustomStrategy } from './my-custom-strategy';

export interface ConfigurableStrategyOptions {
    timeout: number;
    retries: number;
    apiKey: string;
}

export class ConfigurableStrategy implements MyCustomStrategy {
    constructor(private options: ConfigurableStrategyOptions) {}

    async init(injector: Injector): Promise<void> {
        // Use options during initialization
        console.log(`Strategy configured with timeout: ${this.options.timeout}ms`);
    }

    async destroy(): Promise<void> {
        // Cleanup logic
    }

    async processData(ctx: RequestContext, data: any): Promise<string> {
        // Use configuration options
        const timeout = this.options.timeout;
        const retries = this.options.retries;

        // Implementation using these options...
        return 'processed with options';
    }

    validateInput(data: any): boolean {
        return true;
    }
}
```

Usage:

```ts title="vendure-config.ts"
import { ConfigurableStrategy } from './strategies/configurable-strategy';

// In plugin configuration
MyPlugin.init({
    processingStrategy: new ConfigurableStrategy({
        timeout: 5000,
        retries: 3,
        apiKey: process.env.API_KEY,
    }),
});
```

## Multiple Strategies in One Plugin

For complex plugins, you might need multiple strategies:

```ts title="src/types.ts"
export interface ComplexPluginOptions {
    dataProcessingStrategy?: DataProcessingStrategy;
    validationStrategy?: ValidationStrategy;
    cacheStrategy?: CacheStrategy;
}
```

```ts title="src/complex-plugin.ts"
@VendurePlugin({
    // ... plugin config
})
export class ComplexPlugin implements OnApplicationBootstrap, OnApplicationShutdown {
    static options: ComplexPluginOptions;

    static init(options: ComplexPluginOptions) {
        this.options = {
            dataProcessingStrategy: new DefaultDataProcessingStrategy(),
            validationStrategy: new DefaultValidationStrategy(),
            cacheStrategy: new DefaultCacheStrategy(),
            ...options,
        };
        return ComplexPlugin;
    }

    async onApplicationBootstrap() {
        await this.initAllStrategies();
    }

    async onApplicationShutdown() {
        await this.destroyAllStrategies();
    }

    private async initAllStrategies() {
        const injector = new Injector(this.moduleRef);
        const strategies = [
            ComplexPlugin.options.dataProcessingStrategy,
            ComplexPlugin.options.validationStrategy,
            ComplexPlugin.options.cacheStrategy,
        ];

        for (const strategy of strategies) {
            if (strategy && typeof strategy.init === 'function') {
                await strategy.init(injector);
            }
        }
    }

    private async destroyAllStrategies() {
        const strategies = [
            ComplexPlugin.options.dataProcessingStrategy,
            ComplexPlugin.options.validationStrategy,
            ComplexPlugin.options.cacheStrategy,
        ];

        for (const strategy of strategies) {
            if (strategy && typeof strategy.destroy === 'function') {
                await strategy.destroy();
            }
        }
    }
}
```

## Best Practices

### 1. Always Extend InjectableStrategy

```ts
export interface MyStrategy extends InjectableStrategy {
    // ... strategy methods
}
```

### 2. Provide Sensible Defaults

Always provide a default implementation so users can use your plugin out-of-the-box:

```ts
static init(options: MyPluginOptions) {
    this.options = {
        myStrategy: new DefaultMyStrategy(),
        ...options,
    };
    return MyPlugin;
}
```

### 3. Handle Lifecycle Properly

Always implement proper init/destroy handling in your plugin:

```ts
async onApplicationBootstrap() {
    await this.initStrategies();
}

async onApplicationShutdown() {
    await this.destroyStrategies();
}
```

### 4. Use TypeScript for Better DX

Provide strong typing for better developer experience:

```ts
export interface MyStrategy extends InjectableStrategy {
    processData<T>(ctx: RequestContext, data: T): Promise<ProcessedResult<T>>;
}
```

### 5. Document Your Strategy Interface

Provide comprehensive JSDoc comments:

```ts
export interface MyStrategy extends InjectableStrategy {
    /**
     * @description
     * Processes the input data and returns a transformed result.
     * This method is called for each data processing request.
     *
     * @param ctx - The current request context
     * @param data - The input data to process
     * @returns Promise resolving to the processed result
     */
    processData(ctx: RequestContext, data: any): Promise<string>;
}
```

## Summary

Custom strategies in plugins provide a powerful way to make your plugins extensible and configurable. By following the patterns outlined in this guide, you can:

- Define clear strategy interfaces that extend `InjectableStrategy`
- Provide default implementations that work out-of-the-box
- Allow users to inject dependencies through the `init()` method
- Properly manage strategy lifecycle with `init()` and `destroy()` methods
- Enable users to provide their own implementations
- Support configuration options for strategies

This approach ensures your plugins are flexible, maintainable, and follow Vendure's established conventions.
