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:
- Knows how to create services
- Knows what dependencies each service needs
- Shares services across the application
- 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.