Core Architecture - Service Container

The Service Container is one of the most important building blocks of Drupal’s core architecture. Starting with Drupal 8, Drupal moved to a modern, object‑oriented architecture built on top of Symfony components. At the center of this architecture is the Dependency Injection Service Container.

If you want to understand how Drupal core really works, not just how to write features, but how Drupal is designed, extended, tested, and scaled, you must understand the service container.

Why the Service Container Belongs in Core Architecture

The service container is not a feature. It is an architectural pattern used everywhere in Drupal core.

Drupal core uses the service container to:

  • Build and manage objects
  • Inject dependencies into classes
  • Decouple business logic from framework code
  • Enable testing and extensibility
  • Improve performance through shared services

Controllers, forms, plugins, event subscribers, queue workers, REST resources, migrations, cron jobs, and even core subsystems rely on services.

Without understanding the service container, it is very hard to reason about Drupal core behavior.

What Is a Service in Drupal

A service is a reusable PHP object that performs a single responsibility and is managed by the service container.

Examples of common responsibilities:

  • Loading and querying entities
  • Reading and writing configuration
  • Logging
  • Caching
  • Rendering
  • Database access

A service is usually:

  • Stateless
  • Registered in a services.yml file
  • Injected into other classes

What Is the Service Container

The service container is an object that:

  1. Knows how to create services
  2. Knows what dependencies each service needs
  3. Shares services across the application
  4. Injects services where required

Drupal uses Symfony’s DependencyInjection component as the foundation for its container.

In simple terms:

  • You define what a service needs
  • The container builds it correctly
  • Drupal passes it to your class

You do not manually create most objects in modern Drupal.

Why Drupal Introduced the Service Container (Drupal 8+)

Before Drupal 8, Drupal relied heavily on:

  • Global functions
  • Static calls
  • Procedural code

This made large systems hard to test, hard to refactor, and hard to extend.

Drupal 8 introduced:

  • Object oriented programming
  • Dependency injection
  • Clear separation of concerns

The service container made this transition possible.

Defining a Custom Service

Services are defined in a module’s services.yml file.

Example: my_module.services.yml

services:
  my_module.example_service:
    class: Drupal\my_module\Service\ExampleService
    arguments:
      - '@entity_type.manager'
      - '@logger.channel.my_module'

Key concepts:

  • The service ID is how Drupal references the service
  • The class is the PHP implementation
  • Arguments are other services injected into it
  • The @ symbol means fetch from the container

Creating the Service Class

namespace Drupal\my_module\Service;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Psr\Log\LoggerInterface;

class ExampleService {

  protected EntityTypeManagerInterface $entityTypeManager;
  protected LoggerInterface $logger;

  public function __construct(
    EntityTypeManagerInterface $entityTypeManager,
    LoggerInterface $logger
  ) {
    $this->entityTypeManager = $entityTypeManager;
    $this->logger = $logger;
  }

  public function doSomething(): void {
    $this->logger->info('Example service executed');
  }
}

This pattern is fully compatible with Drupal 8 and follows best practices for Drupal 10 and 11.

Using Services Correctly

Incorrect Pattern (Avoid)

\Drupal::service('my_module.example_service')->doSomething();

This creates tight coupling and makes testing difficult.

Correct Pattern: Dependency Injection

Services should be injected into the class that needs them.

Injecting a Service into a Controller

use Drupal\Core\Controller\ControllerBase;
use Drupal\my_module\Service\ExampleService;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ExampleController extends ControllerBase {

  protected ExampleService $exampleService;

  public function __construct(ExampleService $exampleService) {
    $this->exampleService = $exampleService;
  }

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('my_module.example_service')
    );
  }

  public function content() {
    $this->exampleService->doSomething();
    return ['#markup' => 'Service executed'];
  }
}

Injecting Services into Forms

use Drupal\Core\Form\FormBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\my_module\Service\ExampleService;

class ExampleForm extends FormBase {

  protected ExampleService $exampleService;

  public function __construct(ExampleService $exampleService) {
    $this->exampleService = $exampleService;
  }

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('my_module.example_service')
    );
  }
}

Injecting Services into Plugins

Plugins use ContainerFactoryPluginInterface.

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ExamplePlugin implements ContainerFactoryPluginInterface {

  protected ExampleService $exampleService;

  public function __construct(array $configuration, $plugin_id, $plugin_definition, ExampleService $exampleService) {
    $this->exampleService = $exampleService;
  }

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('my_module.example_service')
    );
  }
}

Core Services Every Drupal Developer Should Know

  • entity_type.manager
  • config.factory
  • database
  • current_user
  • cache.default
  • renderer
  • logger.factory
  • messenger

These services appear throughout Drupal core and custom code.

Drupal 10 and 11 Best Practices

  • Always inject services instead of using global calls
  • Use interfaces rather than concrete classes
  • Keep services focused on one responsibility
  • Avoid placing business logic in controllers
  • Do not return render arrays from services

When Using \Drupal::service() Is Acceptable

Limited cases only:

  • Procedural hooks
  • .module files
  • Legacy code

Even in these cases, usage should be minimal.

How to Think About Services Architecturally

  • Controllers handle HTTP requests
  • Forms handle user interaction
  • Plugins provide extensibility
  • Services contain business logic

This separation is the foundation of Drupal’s core architecture.

Summary

The service container is the backbone of modern Drupal. It enables clean architecture, testable code, and scalable systems. Understanding it is essential for reading Drupal core code, writing high quality custom modules, and working confidently on large Drupal 10 and 11 projects.

This article lays the foundation for advanced core architecture topics such as event dispatching, service decoration, compiler passes, and container compilation.