Skip to content

Services for Shared Logic

When to Use

When you have business logic, API integrations, calculations, or data transformations that are used in multiple places (controllers, forms, event subscribers, Drush commands).

Decision

If you need... Use... Why
Logic used in 2+ places Extract to service Single source of truth, testable, reusable
API integration Service with HTTP client injected Centralized, mockable for testing
Complex calculations Service method Avoids duplication, easier to test
One-off controller logic Keep in controller Don't abstract prematurely (Rule of Three)
Utility functions Static utility class OR service Service preferred for DI benefits

Pattern

// mymodule/src/MyApiService.php
namespace Drupal\mymodule;

use Drupal\Core\Config\ConfigFactoryInterface;
use GuzzleHttp\ClientInterface;

class MyApiService {

  public function __construct(
    protected ClientInterface $httpClient,
    protected ConfigFactoryInterface $configFactory,
  ) {}

  public function fetchData(string $endpoint): array {
    $config = $this->configFactory->get('mymodule.settings');
    $base_url = $config->get('api.endpoint');
    $response = $this->httpClient->get($base_url . $endpoint);
    return json_decode($response->getBody(), TRUE);
  }
}
# mymodule/mymodule.services.yml
services:
  mymodule.api:
    class: Drupal\mymodule\MyApiService
    arguments: ['@http_client', '@config.factory']
// Use in multiple places (controller, form, Drush command)
$data = $this->apiService->fetchData('/users');

Reference: core/core.services.yml, core/lib/Drupal/Core/DependencyInjection/ContainerInjectionInterface.php

Common Mistakes

  • Static \Drupal:: calls instead of dependency injection — Hides dependencies, untestable, violates DRY by duplicating container access
  • God services with too many responsibilities — Keep services focused (Single Responsibility Principle); if your service does "everything", split it
  • Premature service creation — Wait until you have 2-3 consumers before extracting (Rule of Three)
  • Constructor logic beyond property assignment — Constructors should only assign injected dependencies to properties, not perform operations
  • Not using factories when needed — For cache bins, loggers, HTTP clients, use factory services (e.g., logger.factory, not logger.channel.default)

See Also