diff --git a/core/core.services.yml b/core/core.services.yml
index f3c6cd9..73de3f3 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -162,6 +162,8 @@ services:
   theme.negotiator:
     class: Drupal\Core\Theme\ThemeNegotiator
     arguments: ['@access_check.theme', '@request_stack']
+    tags:
+      - { name: compiler_pass, tag: theme_negotiator, method: addNegotiator }
   theme.negotiator.default:
     class: Drupal\Core\Theme\DefaultNegotiator
     arguments: ['@config.factory']
@@ -301,6 +303,8 @@ services:
     arguments: ['@router.route_provider']
     calls:
       - [setFinalMatcher, ['@router.matcher.final_matcher']]
+    tags:
+      - { name: compiler_pass, tag: route_filter, method: addRouteFilter }
   url_generator:
     class: Drupal\Core\Routing\UrlGenerator
     arguments: ['@router.route_provider', '@path_processor_manager', '@route_processor_manager', '@config.factory', '@settings']
@@ -629,6 +633,8 @@ services:
   breadcrumb:
     class: Drupal\Core\Breadcrumb\BreadcrumbManager
     arguments: ['@module_handler']
+    tags:
+      - { name: compiler_pass, tag: breadcrumb_builder, method: addBuilder }
   token:
     class: Drupal\Core\Utility\Token
     arguments: ['@module_handler']
diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index 184c767..2faca69 100644
--- a/core/lib/Drupal/Core/CoreServiceProvider.php
+++ b/core/lib/Drupal/Core/CoreServiceProvider.php
@@ -14,20 +14,18 @@
 use Drupal\Core\DependencyInjection\ServiceProviderInterface;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DependencyInjection\Compiler\ModifyServiceDefinitionsPass;
+use Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterKernelListenersPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterAccessChecksPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterPathProcessorsPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterRouteProcessorsPass;
-use Drupal\Core\DependencyInjection\Compiler\RegisterRouteFiltersPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterRouteEnhancersPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterStringTranslatorsPass;
-use Drupal\Core\DependencyInjection\Compiler\RegisterBreadcrumbBuilderPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterAuthenticationPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterTwigExtensionsPass;
 use Drupal\Core\Plugin\PluginManagerPass;
-use Drupal\Core\Theme\ThemeNegotiatorPass;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\DependencyInjection\Reference;
 use Symfony\Component\DependencyInjection\Definition;
@@ -61,7 +59,12 @@ public function register(ContainerBuilder $container) {
     // service definitions. This pass must come first so that later
     // list-building passes are operating on the post-alter services list.
     $container->addCompilerPass(new ModifyServiceDefinitionsPass());
-    $container->addCompilerPass(new RegisterRouteFiltersPass());
+
+    // Collect and apply all tagged handler services as method calls to consumer
+    // services.
+    // @todo Needs to run last, after all container build + alter stages.
+    $container->addCompilerPass(new TaggedHandlersPass());
+
     // Add a compiler pass for registering event subscribers.
     $container->addCompilerPass(new RegisterKernelListenersPass(), PassConfig::TYPE_AFTER_REMOVING);
     $container->addCompilerPass(new RegisterAccessChecksPass());
@@ -77,12 +80,6 @@ public function register(ContainerBuilder $container) {
     $container->addCompilerPass(new CacheContextsPass());
     // Add the compiler pass for appending string translators.
     $container->addCompilerPass(new RegisterStringTranslatorsPass());
-    // Add the compiler pass that will process the tagged breadcrumb builder
-    // services.
-    $container->addCompilerPass(new RegisterBreadcrumbBuilderPass());
-    // Add the compiler pass that will process the tagged theme negotiator
-    // service.
-    $container->addCompilerPass(new ThemeNegotiatorPass());
     // Add the compiler pass that will process the tagged config factory
     // override services.
     $container->addCompilerPass(new ConfigFactoryOverridePass());
diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterBreadcrumbBuilderPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterBreadcrumbBuilderPass.php
deleted file mode 100644
index 6cc73af..0000000
--- a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterBreadcrumbBuilderPass.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\DependencyInjection\Compiler\RegisterBreadcrumbBuilderPass.
- */
-
-namespace Drupal\Core\DependencyInjection\Compiler;
-
-use Symfony\Component\DependencyInjection\Reference;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
-
-/**
- * Adds services to the breadcrumb_builder service.
- */
-class RegisterBreadcrumbBuilderPass implements CompilerPassInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function process(ContainerBuilder $container) {
-    if (!$container->hasDefinition('breadcrumb')) {
-      return;
-    }
-    $manager = $container->getDefinition('breadcrumb');
-    if (is_subclass_of($manager->getClass(), 'Drupal\Core\Breadcrumb\ChainBreadcrumbBuilderInterface')) {
-      foreach ($container->findTaggedServiceIds('breadcrumb_builder') as $id => $attributes) {
-        $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
-        $manager->addMethodCall('addBuilder', array(new Reference($id), $priority));
-      }
-    }
-  }
-
-}
diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteFiltersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteFiltersPass.php
deleted file mode 100644
index da47fca..0000000
--- a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteFiltersPass.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains Drupal\Core\DependencyInjection\Compiler\RegisterRouteFiltersPass.
- */
-
-namespace Drupal\Core\DependencyInjection\Compiler;
-
-use Symfony\Component\DependencyInjection\Reference;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
-
-/**
- * Adds services tagged 'router.matcher' to the matcher service.
- */
-class RegisterRouteFiltersPass implements CompilerPassInterface {
-
-  /**
-   * Adds services tagged 'router.matcher' to the matcher service.
-   *
-   * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
-   *   The container to process.
-   */
-  public function process(ContainerBuilder $container) {
-    if (!$container->hasDefinition('router.matcher')) {
-      return;
-    }
-    $matcher = $container->getDefinition('router.matcher');
-    foreach ($container->findTaggedServiceIds('route_filter') as $id => $attributes) {
-      $matcher->addMethodCall('addRouteFilter', array(new Reference($id)));
-    }
-  }
-}
diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
new file mode 100644
index 0000000..6aac0ef
--- /dev/null
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass.
+ */
+
+namespace Drupal\Core\DependencyInjection\Compiler;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * Adds tagged handlers to corresponding consumer services.
+ */
+class TaggedHandlersPass implements CompilerPassInterface {
+
+  /**
+   * {@inheritdoc}
+   *
+   * Finds all services tagged with 'compiler_pass' and adds method calls for
+   * all tagged handlers/providers for it to its definition.
+   *
+   * Supported compiler_pass tag attributes:
+   * - tag: (optional) The tag name used by handler services to collect.
+   *   Defaults to the service ID of the consumer.
+   * - method: (optional) The method name to call on the consumer service.
+   *   Defaults to 'addHandler'. This method gets the handler instance as first
+   *   argument and the handler's priority as second, whereas all handlers
+   *   discovered at compile time are sorted already.
+   * - require: (optional) A fully qualified interface name to validate before
+   *   adding handlers.
+   *
+   * Supported handler tag attributes:
+   * - priority: (optional) An integer denoting the priority of the handler.
+   *   Defaults to 0.
+   */
+  public function process(ContainerBuilder $container) {
+    foreach ($container->findTaggedServiceIds('compiler_pass') as $consumer_id => $consumer_attributes) {
+      $consumer_attributes = $consumer_attributes[0];
+      // Ensure defaults.
+      $consumer_attributes += array(
+        'tag' => $consumer_id,
+        'method' => 'addHandler',
+      );
+      // Find all tagged handlers.
+      $handlers = array();
+      foreach ($container->findTaggedServiceIds($consumer_attributes['tag']) as $id => $attributes) {
+        // If an interface constraint exists, ensure that every handler meets it.
+        if (isset($consumer_attributes['require'])) {
+          $handler = $container->getDefinition($id);
+          // @todo Shouldn't this rather blow up with an exception?
+          if (!is_subclass_of($handler->getClass(), $consumer_attributes['require'])) {
+            continue;
+          }
+        }
+        $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
+      }
+      if (empty($handlers)) {
+        continue;
+      }
+      // Sort all handlers by priority.
+      arsort($handlers, SORT_NUMERIC);
+
+      // Add a method call for each handler to the consumer service definition.
+      $consumer = $container->getDefinition($consumer_id);
+      foreach ($handlers as $id => $priority) {
+        $consumer->addMethodCall($consumer_attributes['method'], array(new Reference($id), $priority));
+      }
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Theme/ThemeNegotiatorPass.php b/core/lib/Drupal/Core/Theme/ThemeNegotiatorPass.php
deleted file mode 100644
index 7f14692..0000000
--- a/core/lib/Drupal/Core/Theme/ThemeNegotiatorPass.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Theme\ThemeNegotiatorPass.
- */
-
-namespace Drupal\Core\Theme;
-
-use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\Reference;
-
-/**
- * Adds services to the theme negotiator service.
- *
- * @see \Drupal\Core\Theme\ThemeNegotiator
- * @see \Drupal\Core\Theme\ThemeNegotiatorInterfa
- */
-class ThemeNegotiatorPass implements CompilerPassInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function process(ContainerBuilder $container) {
-    if (!$container->hasDefinition('theme.negotiator')) {
-      return;
-    }
-    $manager = $container->getDefinition('theme.negotiator');
-    foreach ($container->findTaggedServiceIds('theme_negotiator') as $id => $attributes) {
-      $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
-      $manager->addMethodCall('addNegotiator', array(new Reference($id), $priority));
-    }
-  }
-
-}
