Make Your App Grow Painlessly: How I Escaped Repetitive Code Thanks to the Provider Pattern

A few days ago I wrote about Docker DB Manager, a desktop application that simplifies managing database containers in Docker.

But there was a problem: every time I added support for a new database, I had to write duplicate code in both Rust and TypeS…


This content originally appeared on DEV Community and was authored by AbianS


A few days ago I wrote about Docker DB Manager, a desktop application that simplifies managing database containers in Docker.

But there was a problem: every time I added support for a new database, I had to write duplicate code in both Rust and TypeScript. It was tedious, error-prone, and frustrating.

Today I'll tell you how I completely transformed the architecture using the Provider pattern, turning the addition of new databases into something as simple as writing a single TypeScript class.

What is the Provider Pattern?

The Provider pattern is a design pattern that allows you to delegate the responsibility of providing specific functionality to interchangeable components. Instead of having centralized logic with conditionals for each case, each provider implements a common interface and handles its own configuration and behavior.

The core idea: Instead of asking "what type are you?" and acting accordingly, we ask the object: "what do you need?" and it provides it.

This pattern is especially useful when:

  • You have multiple implementations of the same functionality
  • Each implementation has its own specific logic
  • You want to add new implementations without modifying existing code
  • You need to keep the code decoupled and scalable

The Problem: Duplicate Code Everywhere

Backend in Rust with Specific Logic

In docker.rs I had a giant method with a match statement for each database:

pub fn build_docker_command(
    &self,
    request: &CreateDatabaseRequest,
    volume_name: &Option<String>,
) -> Result<Vec<String>, String> {
    let mut args = vec![
        "run".to_string(),
        "-d".to_string(),
        "--name".to_string(),
        request.name.clone(),
        "-p".to_string(),
        format!("{}:{}", request.port, self.get_default_port(&request.db_type)),
    ];

    // Add volume if persist_data is true
    if let Some(vol_name) = volume_name {
        args.push("-v".to_string());
        args.push(format!("{}:{}", vol_name, self.get_data_path(&request.db_type)));
    }

    // Environment variables based on database type
    match request.db_type.as_str() {
        "PostgreSQL" => {
            args.push("-e".to_string());
            args.push(format!("POSTGRES_PASSWORD={}", request.password));

            if let Some(username) = &request.username {
                if username != "postgres" {
                    args.push("-e".to_string());
                    args.push(format!("POSTGRES_USER={}", username));
                }
            }
            // ... more PostgreSQL-specific configuration
        }
        "MySQL" => {
            args.push("-e".to_string());
            args.push(format!("MYSQL_ROOT_PASSWORD={}", request.password));
            // ... more MySQL-specific configuration
        }
        "MongoDB" => {
            // ... MongoDB-specific configuration
        }
        "Redis" => {
            // ... Redis-specific configuration
        }
        // ... more cases for each database
        _ => return Err(format!("Database type not supported: {}", request.db_type)),
    }

    Ok(args)
}

Frontend with Infinite Conditionals

// In container-configuration-step.tsx (561 lines!)
function ContainerConfigurationStep() {
  // Different fields based on database type
  if (selectedDatabase === 'PostgreSQL') {
    return (
      <div>
        <Input label="Username" defaultValue="postgres" />
        <Input label="Password" type="password" />
        <Select label="Host Auth Method">
          <option>md5</option>
          <option>scram-sha-256</option>
        </Select>
        {/* ... 100 more lines of specific configuration */}
      </div>
    )
  }

  if (selectedDatabase === 'MySQL') {
    return (
      <div>
        <Input label="Root Password" type="password" />
        <Select label="Character Set">
          <option>utf8mb4</option>
          <option>utf8</option>
        </Select>
        {/* ... other specific configuration */}
      </div>
    )
  }

  // ... more ifs for MongoDB, Redis, etc.
}

The Real Cost

Adding support for a new database required:

  1. Backend (Rust):

    • Modify docker.rs (new match case)
    • Update types in models.rs
    • Add default values in multiple functions
    • Update tests
  2. Frontend (TypeScript):

    • Add form fields in container-configuration-step.tsx
    • Update types
    • Modify validation logic
    • Adjust UI conditionals

Result: Numerous files modified, significant development time required, increased risk of breaking things.

The Solution: The Provider Pattern

The key idea was simple but powerful: each database should know how to configure itself.

The DatabaseProvider Interface

I created an interface that every database must implement:

export interface DatabaseProvider {
  // Basic metadata
  getName(): string;
  getDisplayName(): string;
  getIcon(): string;
  getAvailableVersions(): string[];
  getDefaultVersion(): string;

  // Form rendering
  renderConfigurationFields(form: UseFormReturn<any>): React.ReactNode;

  // Docker logic
  buildDockerArgs(config: DatabaseConfiguration): DockerArgs;

  // Validation
  validateConfiguration(config: DatabaseConfiguration): ValidationResult;
}

Concrete Implementation: PostgreSQL

export class PostgreSQLDatabaseProvider implements DatabaseProvider {
  getName(): string {
    return 'postgresql';
  }

  getDisplayName(): string {
    return 'PostgreSQL';
  }

  getIcon(): string {
    return '/postgresql-icon.svg';
  }

  getAvailableVersions(): string[] {
    return ['17', '16', '15', '14', '13'];
  }

  getDefaultVersion(): string {
    return '16';
  }

  renderConfigurationFields(form: UseFormReturn<any>) {
    return (
      <>
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Password</FormLabel>
              <FormControl>
                <Input type="password" {...field} />
              </FormControl>
            </FormItem>
          )}
        />
        {/* ... more PostgreSQL-specific fields */}
      </>
    );
  }

  buildDockerArgs(config: DatabaseConfiguration): DockerArgs {
    const envVars: Record<string, string> = {
      POSTGRES_PASSWORD: config.password,
    };

    if (config.username && config.username !== 'postgres') {
      envVars.POSTGRES_USER = config.username;
    }

    if (config.database) {
      envVars.POSTGRES_DB = config.database;
    }

    return {
      image: `postgres:${config.version}`,
      ports: [`${config.port}:5432`],
      envVars,
      volumes: config.persistData
        ? [{
            name: `${config.name}-data`,
            mountPath: '/var/lib/postgresql/data',
          }]
        : [],
    };
  }

  validateConfiguration(config: DatabaseConfiguration): ValidationResult {
    const errors: string[] = [];

    if (!config.password || config.password.length < 8) {
      errors.push('Password must be at least 8 characters');
    }

    return {
      isValid: errors.length === 0,
      errors,
    };
  }
}

The Database Registry

A simple registry to manage all providers:

class DatabaseRegistry {
  private providers = new Map<string, DatabaseProvider>();

  register(provider: DatabaseProvider): void {
    this.providers.set(provider.getName(), provider);
  }

  get(name: string): DatabaseProvider | undefined {
    return this.providers.get(name);
  }

  getAll(): DatabaseProvider[] {
    return Array.from(this.providers.values());
  }
}

// Global instance
export const databaseRegistry = new DatabaseRegistry();

// Register all databases
databaseRegistry.register(new PostgreSQLDatabaseProvider());
databaseRegistry.register(new MySQLDatabaseProvider());
databaseRegistry.register(new MongoDBDatabaseProvider());
databaseRegistry.register(new RedisDatabaseProvider());

Using the Pattern in the UI

The configuration form became incredibly simple:

function ContainerConfigurationStep() {
  const form = useForm<DatabaseConfiguration>();
  const selectedDatabase = form.watch('databaseType');

  // Get the provider for the selected database
  const provider = databaseRegistry.get(selectedDatabase);

  if (!provider) {
    return <div>Select a database</div>;
  }

  return (
    <Form {...form}>
      {/* Render provider-specific fields */}
      {provider.renderConfigurationFields(form)}
    </Form>
  );
}

From 561 lines to ~50 lines. The form doesn't need to know anything about databases, just that they have providers.

Backend Transformation

The Rust backend became completely database-agnostic:

// Frontend sends complete Docker arguments
const provider = databaseRegistry.get(config.databaseType);
const dockerArgs = provider.buildDockerArgs(config);

// Send to backend via Tauri
await invoke('create_database', {
  request: {
    name: config.name,
    docker_args: dockerArgs,
  },
});
// Backend (Rust) - completely generic
#[tauri::command]
pub async fn create_database(
    app: AppHandle,
    request: CreateDatabaseRequest,
) -> Result<Database, String> {
    let docker_service = DockerService::new();

    // Create volumes
    for volume in &request.docker_args.volumes {
        docker_service.create_volume_if_needed(&app, &volume.name).await?;
    }

    // Build Docker command (generic for all DBs)
    let docker_args = docker_service
        .build_docker_command_from_args(&request.name, &request.docker_args);

    // Execute
    let container_id = docker_service.run_container(&app, &docker_args).await?;

    Ok(database)
}

The build_docker_command_from_args method is completely generic. The provider already built all the arguments in the frontend.

Benefits of the Provider Pattern

1. Extensibility Without Modifying Existing Code

The Open/Closed principle (open for extension, closed for modification) of SOLID is perfectly fulfilled. Adding a new database doesn't require touching existing code:

2. Separation of Concerns

Each provider is responsible for:

  • Its own configuration
  • Its own form fields
  • Its own Docker logic
  • Its own validation

The consuming code only needs to know that a provider exists, not how it works internally.

3. Simplified Testing

Each provider has its own tests, isolated and easy to maintain:

describe('MySQLDatabaseProvider', () => {
  const provider = new MySQLDatabaseProvider();

  it('should build correct docker args', () => {
    const config = {
      name: 'test-mysql',
      port: 3306,
      version: '8.0',
      password: 'secret123',
    };

    const args = provider.buildDockerArgs(config);

    expect(args.image).toBe('mysql:8.0');
    expect(args.envVars.MYSQL_ROOT_PASSWORD).toBe('secret123');
  });
});

Conclusion

The Provider pattern completely transformed Docker DB Manager's architecture. What previously required changes in 19 different files is now solved by creating a single file that implements the interface.

This pattern is especially powerful when you have multiple implementations of the same functionality. Whether it's databases, authentication providers, payment processors, or any interchangeable component, the Provider pattern allows you to scale without accumulating technical debt.

If your project requires changes in multiple places to add a feature, you probably need to identify the right pattern. Not because the code is "bad," but because a better abstraction can transform hours of work into minutes.

Want to see the code or contribute?

Docker DB Manager is free, open source, and currently available for macOS (Windows and Linux in development).

Have you applied the Provider pattern in your projects? Have questions about software architecture? Leave me a comment!


This content originally appeared on DEV Community and was authored by AbianS


Print Share Comment Cite Upload Translate Updates
APA

AbianS | Sciencx (2025-10-16T15:43:16+00:00) Make Your App Grow Painlessly: How I Escaped Repetitive Code Thanks to the Provider Pattern. Retrieved from https://www.scien.cx/2025/10/16/make-your-app-grow-painlessly-how-i-escaped-repetitive-code-thanks-to-the-provider-pattern/

MLA
" » Make Your App Grow Painlessly: How I Escaped Repetitive Code Thanks to the Provider Pattern." AbianS | Sciencx - Thursday October 16, 2025, https://www.scien.cx/2025/10/16/make-your-app-grow-painlessly-how-i-escaped-repetitive-code-thanks-to-the-provider-pattern/
HARVARD
AbianS | Sciencx Thursday October 16, 2025 » Make Your App Grow Painlessly: How I Escaped Repetitive Code Thanks to the Provider Pattern., viewed ,<https://www.scien.cx/2025/10/16/make-your-app-grow-painlessly-how-i-escaped-repetitive-code-thanks-to-the-provider-pattern/>
VANCOUVER
AbianS | Sciencx - » Make Your App Grow Painlessly: How I Escaped Repetitive Code Thanks to the Provider Pattern. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/10/16/make-your-app-grow-painlessly-how-i-escaped-repetitive-code-thanks-to-the-provider-pattern/
CHICAGO
" » Make Your App Grow Painlessly: How I Escaped Repetitive Code Thanks to the Provider Pattern." AbianS | Sciencx - Accessed . https://www.scien.cx/2025/10/16/make-your-app-grow-painlessly-how-i-escaped-repetitive-code-thanks-to-the-provider-pattern/
IEEE
" » Make Your App Grow Painlessly: How I Escaped Repetitive Code Thanks to the Provider Pattern." AbianS | Sciencx [Online]. Available: https://www.scien.cx/2025/10/16/make-your-app-grow-painlessly-how-i-escaped-repetitive-code-thanks-to-the-provider-pattern/. [Accessed: ]
rf:citation
» Make Your App Grow Painlessly: How I Escaped Repetitive Code Thanks to the Provider Pattern | AbianS | Sciencx | https://www.scien.cx/2025/10/16/make-your-app-grow-painlessly-how-i-escaped-repetitive-code-thanks-to-the-provider-pattern/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.