In modern Drupal development, the service container and dependency injection have replaced global static calls like \Drupal::currentUser() as the standard way to access reusable functionality. In plain terms, this is about a cleaner way to organize your Drupal code so it stays easy to test, fix, and reuse.

The service container is Drupal's central registry for PHP service objects. It manages, instantiates, and reuses them so you never build them by hand. Dependency injection is the pattern that passes those objects into a class through its constructor, instead of pulling them from a global service locator. If you're building custom modules, controllers, plugins, or event subscribers, understanding both isn't optional. It's a necessity.

You'll still see code like this in older modules:

$user = \Drupal::currentUser();

That pattern works. But Drupal's move to a Symfony-based architecture has made it the exception, not the standard.

Key Takeaways

  • The service container centrally registers and manages reusable PHP service objects, so you never have to instantiate them by hand.
  • Dependency Injection passes a class's dependencies through its constructor instead of pulling them from \Drupal::service(), which keeps code testable and maintainable.
  • Custom services are defined in a module's *.services.yml file, built as a class, and pulled into controllers through the create() method.
  • Drupal 11 adds PHP 8+ constructor property promotion, letting you write leaner service classes.
  • Avoid calling \Drupal::service() inside classes, overloading a class with too many injected services, and type-hinting concrete classes instead of interfaces.

What Is a Service in Drupal?

A service in Drupal is just a PHP object that helps you do certain things. Instead of instantiating objects all over your code, Drupal lets you register them in one place so they can be reused easily.

These are some of the common services in core Drupal:

  • entity_type.manager — loads and manages entities
  • logger.factory — logs application activity
  • config.factory — reads application configs
  • path.current — gets the current path of the page

All these services are registered in .services.yml files and stored in Drupal's service container.

What Is the Service Container?

The service container, or Dependency Injection Container, is the central registry of all services in Drupal. It's based on Symfony's Dependency Injection Component and is used for:

  • Registering and defining services
  • Instantiating them when needed
  • Managing their dependencies
  • Reusing them efficiently throughout the application

Here's how a basic service definition looks in a custom module:

services:

  my_module.custom_service:
    class: Drupal\my_module\CustomService
    arguments: ['@current_user', '@logger.factory']

The @ symbol indicates that these are other services that have already been registered. The service container takes care of everything for you.

What Is Dependency Injection?

Dependency Injection is a design pattern where an object receives its dependencies from outside instead of creating them on its own.

Here's how it looks with and without Dependency Injection.

This is the core difference between a service locator and dependency injection: one pattern reaches into the container from inside the class, the other has the container hand the dependency to the class. 

Without Dependency Injection:

class CustomClass {
  public function loadNode($nid) {
    $entity_type_manager = \Drupal::service('entity_type.manager');
    return $entity_type_manager->getStorage('node')->load($nid);
  }
}

This code will work, but it tightly couples your class to the global service locator provided by Drupal. That makes it difficult to test and maintain, and it violates Drupal's coding standards.

Using Dependency Injection:

use Drupal\Core\Entity\EntityTypeManagerInterface;
class CustomClass {
  protected $entityTypeManager;
  public function __construct(EntityTypeManagerInterface $entityTypeManager) {
    $this->entityTypeManager = $entityTypeManager;
  }
  public function loadNode($nid) {
    return $this->entityTypeManager->getStorage('node')->load($nid);
  }
}

Here, the dependency is injected into the class through the constructor. The class doesn't care how EntityTypeManager is created. It just uses it. That makes the code clean and testable.

How Do You Create a Basic Custom Service in Drupal?

Now let's see how to create a simple custom service from scratch.

Step 1: Define the service

services:

  my_module.custom_service:
    class: Drupal\my_module\CustomService
    arguments: ['@current_user', '@logger.factory']

Step 2: Add the service class

namespace Drupal\my_module;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
class CustomService{
  protected $currentUser;
  protected $logger;
  public function __construct(AccountProxyInterface $current_user, LoggerChannelFactoryInterface $logger_factory) {
    $this->currentUser = $current_user;
    $this->logger = $logger_factory->get('my_module');
  }
  public function customFunc() {
    $name = $this->currentUser->getDisplayName();
    $this->logger->info('Logger info @name', ['@name' => $name]);
    return "Hello, $name!";
  }
}

Step 3: Use it in a controller

use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\my_module\CustomService;
class GreetingController extends ControllerBase {
  protected $customService;
  public function __construct(CustomService $custom_service) {
    $this->customService = $custom_service;
  }
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('my_module.custom_service')
    );
  }
  public function content() {
    return [
      '#markup' => $this->customService->customFunc(),
    ];
  }
}

The create() method is Drupal's way of pulling services from the container and passing them into the constructor. This is the standard pattern for controllers, forms, and plugins.

Why Does Dependency Injection Matter in Projects?

In small, quick builds, calling \Drupal::service() might feel harmless. But in larger websites and enterprise-scale applications, the difference becomes significant.

Testability. Services that are injected can be easily mocked in unit tests. Static calls can't be replaced as easily, which makes automated testing harder.

Maintainability. If a dependency changes, you only need to change it in one place: the service definition. You don't have to search through multiple files.

Separation of concerns. Business logic stays clean and separate from Drupal internals. Each service is responsible for doing one thing well.

Drupal 11 readiness. Drupal 11 has removed several deprecated static service usage patterns and is more in line with Symfony best practices. Using Dependency Injection today means fewer breaking changes when you move to Drupal 11.

What Does Drupal 11 Change for Services and Dependency Injection?

Drupal 11 runs on Symfony 7 and requires PHP 8.2 or higher (Drupal.org lowered this from an initial 8.3 requirement for longer security support). That shift matters for services and DI in three ways.

Constructor property promotion. You can now declare and assign a property in the same line:

public function __construct(
  protected AccountProxyInterface $currentUser,
  protected LoggerChannelFactoryInterface $loggerFactory,
) {}

This replaces four lines of property declaration and constructor assignment with one block.

Read-only properties. Where a service's dependency never changes after construction, mark it readonly. It documents intent and stops accidental reassignment later in the class.

Closer Symfony alignment. Because Drupal 11 tracks Symfony 7 more closely than Drupal 10 tracked Symfony 6, service definitions written to current Symfony conventions carry forward with fewer changes at your next major upgrade.

None of this changes how you register a service in *.services.yml. It changes how little code you need to write once that service reaches your constructor.

What Common Mistakes Should You Avoid With Services and Dependency Injection?

Should I use \Drupal::service() inside a class?

No. Reserve \Drupal::service() for procedural code, like hook implementations. Calling it inside a class ties that class to Drupal's global service locator. That's hard to mock in unit tests and hard to change later.

How many services should I inject into one class?

As few as the class genuinely needs. A constructor with five or six injected services is usually a sign the class is doing too much. Split the responsibilities into smaller, focused services instead.

Should I type-hint interfaces or concrete classes?

Type-hint interfaces wherever Drupal core provides one — EntityTypeManagerInterface instead of EntityTypeManager, for example. Interfaces let you swap implementations later without breaking every class that depends on them.

Final thoughts

Using the service container and Dependency Injection can feel like extra work when you just want something to work quickly. At first, it can seem like an unnecessary setup. But once you use them regularly, your code becomes cleaner and easier to manage. Debugging gets simpler, and your project feels organized instead of messy.

Drupal is moving closer to Symfony and modern PHP practices. These patterns aren't just advanced concepts anymore. They're becoming the normal way of building things properly in Drupal. The sooner you get comfortable with them, the easier your future projects will be. And if you'd rather have Drupal experts handle this the right way from day one, our Drupal development services can help you build clean, maintainable code that scales.

Frequently Asked Questions

What is a service in Drupal? 

A service in Drupal is a PHP object registered in the service container so it can be reused across your code instead of being instantiated repeatedly. Core examples include entity_type.manager, logger.factory, config.factory, and path.current. Every service is defined in a module's *.services.yml file.

What is the Drupal service container? 

The service container, or Dependency Injection Container, is Drupal's central registry for every service. Built on Symfony's Dependency Injection Component, it registers and defines services, instantiates them on demand, manages their dependencies, and lets you reuse them efficiently throughout the application.

What is dependency injection, and why does it matter? 

Dependency injection is a design pattern where a class receives its dependencies through its constructor instead of creating them internally with calls like \Drupal::service(). This keeps classes decoupled from Drupal's global service locator, which makes the code easier to test, maintain, and keep clean.

How do you create a custom service in Drupal? 

Define the service in your module's *.services.yml file with a class and its argument dependencies, such as @current_user and @logger.factory. Then build the service class with a constructor that receives those dependencies. Finally, pull the service into a controller through its create() method, the standard pattern for controllers, forms, and plugins.

Why should I avoid using \Drupal::service() inside a class? 

Calling \Drupal::service() inside a class ties it to Drupal's global service locator, which is hard to mock in unit tests and hard to maintain as dependencies change. Reserve \Drupal::service() for procedural code, like hook implementations, and inject dependencies through the constructor everywhere else.

What does Drupal 11 change for services and dependency injection? 

Drupal 11 removes several deprecated static service usage patterns and leans further into Symfony conventions. It also supports PHP 8+ constructor property promotion, so you can declare and assign properties like protected AccountProxyInterface $currentUser directly in the constructor signature, cutting boilerplate from service classes.

What are the most common dependency injection mistakes to avoid? 

The three most common mistakes are calling \Drupal::service() inside classes instead of injecting dependencies, passing too many services into one class (a sign it's doing too much), and type-hinting concrete classes instead of interfaces, which makes it harder to swap implementations later.

Contact us

LET'S DISCUSS YOUR IDEAS. 
WE'D LOVE TO HEAR FROM YOU.

CONTACT US SUBMIT RFP