diff --git a/core/core.services.yml b/core/core.services.yml
index 459503e44c..a4c7b24198 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1339,6 +1339,9 @@ services:
   token:
     class: Drupal\Core\Utility\Token
     arguments: ['@module_handler', '@cache.default', '@language_manager', '@cache_tags.invalidator', '@renderer']
+  batch.processor:
+    class: Drupal\Core\Batch\BatchQueueController
+    arguments: ['@app.root', '@batch.storage', '@date.formatter', '@form_submitter', '@request_stack']
   batch.storage:
     class: Drupal\Core\Batch\BatchStorage
     arguments: ['@database', '@session', '@csrf_token']
diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 94524e2f71..9955b674a6 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -14,7 +14,6 @@
  * @see batch_get()
  */
 
-use Drupal\Core\Batch\BatchQueueController;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -261,7 +260,9 @@ function _batch_progress_page() {
  * @internal
  */
 function _batch_process() {
-  return BatchQueueController::processQueue();
+  /** @var \Drupal\Core\Batch\BatchQueueControllerInterface $batch_processor */
+  $batch_processor = \Drupal::service('batch.processor');
+  return $batch_processor->processQueue();
 }
 
 /**
@@ -270,7 +271,9 @@ function _batch_process() {
  * @internal
  */
 function &_batch_current_set() {
-  return BatchQueueController::getCurrentSet();
+  /** @var \Drupal\Core\Batch\BatchQueueControllerInterface $batch_processor */
+  $batch_processor = \Drupal::service('batch.processor');
+  return $batch_processor->getCurrentSet();
 }
 
 /**
@@ -287,7 +290,9 @@ function &_batch_current_set() {
  * @internal
  */
 function _batch_next_set() {
-  return BatchQueueController::nextSet();
+  /** @var \Drupal\Core\Batch\BatchQueueControllerInterface $batch_processor */
+  $batch_processor = \Drupal::service('batch.processor');
+  return $batch_processor->nextSet();
 }
 
 /**
@@ -299,7 +304,9 @@ function _batch_next_set() {
  * @internal
  */
 function _batch_finished() {
-  return BatchQueueController::finishedProcessing();
+  /** @var \Drupal\Core\Batch\BatchQueueControllerInterface $batch_processor */
+  $batch_processor = \Drupal::service('batch.processor');
+  return $batch_processor->finishedProcessing();
 }
 
 /**
diff --git a/core/includes/form.inc b/core/includes/form.inc
index 585dbae17b..84c65c01ae 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -7,7 +7,6 @@
 
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Batch\Batch;
-use Drupal\Core\Batch\BatchQueueController;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\Element\RenderElement;
 use Drupal\Core\Template\Attribute;
@@ -751,20 +750,18 @@ function batch_set($batch_definition) {
  *   A redirect response if the batch is progressive. No return value otherwise.
  */
 function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = NULL) {
-  return BatchQueueController::process($redirect, $url, $redirect_callback);
+  /** @var \Drupal\Core\Batch\BatchQueueControllerInterface $batch_processor */
+  $batch_processor = \Drupal::service('batch.processor');
+  return $batch_processor->process($redirect, $url, $redirect_callback);
 }
 
 /**
  * Retrieves the current batch.
  */
 function &batch_get() {
-  // Not drupal_static(), because Batch API operates at a lower level than most
-  // use-cases for resetting static variables, and we specifically do not want a
-  // global drupal_static_reset() resetting the batch information. Functions
-  // that are part of the Batch API and need to reset the batch information may
-  // call batch_get() and manipulate the result by reference. Functions that are
-  // not part of the Batch API can also do this, but shouldn't.
-  return BatchQueueController::getBatches();
+  /** @var \Drupal\Core\Batch\BatchQueueControllerInterface $batch_processor */
+  $batch_processor = \Drupal::service('batch.processor');
+  return $batch_processor->getBatches();
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Batch/Batch.php b/core/lib/Drupal/Core/Batch/Batch.php
index 291b785a2e..8214a3ab8a 100644
--- a/core/lib/Drupal/Core/Batch/Batch.php
+++ b/core/lib/Drupal/Core/Batch/Batch.php
@@ -402,15 +402,17 @@ public function addCss($filename) {
   /**
    * Places the batch in the queue to be processed.
    *
-   * This is an alias for BatchQueueController::enqueue() to allow for
+   * This is an alias for BatchQueueControllerInterface::enqueue() to allow for
    * pipe-lining of Batch objects.
    *
    * @return $this
    *
-   * @see \Drupal\Core\Batch\BatchQueueController::enqueue()
+   * @see \Drupal\Core\Batch\BatchQueueControllerInterface::enqueue()
    */
   public function enqueue() {
-    BatchQueueController::enqueue($this);
+    /** @var \Drupal\Core\Batch\BatchQueueControllerInterface $batch_processor */
+    $batch_processor = \Drupal::service('batch.processor');
+    $batch_processor->enqueue($this);
     return $this;
   }
 
diff --git a/core/lib/Drupal/Core/Batch/BatchQueueController.php b/core/lib/Drupal/Core/Batch/BatchQueueController.php
index 04ffd851ef..a29d38b95b 100644
--- a/core/lib/Drupal/Core/Batch/BatchQueueController.php
+++ b/core/lib/Drupal/Core/Batch/BatchQueueController.php
@@ -5,19 +5,29 @@
 use Drupal\Component\Utility\Timer;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Database\Database;
+use Drupal\Core\Datetime\DateFormatterInterface;
 use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormSubmitterInterface;
 use Drupal\Core\Queue\Batch as BatchQueue;
 use Drupal\Core\Queue\BatchMemory;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
 use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Handles operations on the batch processes to be handled.
  *
  * @internal
  */
-class BatchQueueController {
+class BatchQueueController implements BatchQueueControllerInterface {
+
+  /**
+   * The app root.
+   *
+   * @var string;
+   */
+  protected $root;
 
   /**
    * The batch sets to be processed.
@@ -26,7 +36,7 @@ class BatchQueueController {
    *
    * @var array
    */
-  protected static $batches = [];
+  protected $batches;
 
   /**
    * The queue objects to be processed.
@@ -35,23 +45,68 @@ class BatchQueueController {
    *
    * @var array
    */
-  protected static $queues = [];
+  protected $queues;
 
   /**
-   * Places the batch in the queue to be processed.
+   * The batch storage service.
    *
-   * Batch objects are also queued by invoking <code>enqueue()</code> directly
-   * on the object.
+   * @var \Drupal\Core\Batch\BatchStorageInterface
+   */
+  protected $batchStorage;
+
+  /**
+   * The date formatter used to calculate the needed time for the batch.
    *
-   * @param \Drupal\Core\Batch\Batch $batch
-   *   The batch to place in the queue.
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * The form submitter used to redirect at the end of the batch.
    *
-   * @see \Drupal\Core\Batch\Batch::enqueue()
+   * @var \Drupal\Core\Form\FormSubmitterInterface
+   */
+  protected $formSubmitter;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * Creates a new BatchQueueController.
+   *
+   * @param string $root
+   *   The app root.
+   * @param \Drupal\Core\Batch\BatchStorageInterface $batch_storage
+   *   The batch storage.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter used to calculate the needed time for the batch.
+   * @param \Drupal\Core\Form\FormSubmitterInterface $form_submitter
+   *   The form submitter used to redirect at the end of the batch.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   */
+  public function __construct($root, BatchStorageInterface $batch_storage, DateFormatterInterface $date_formatter, FormSubmitterInterface $form_submitter, RequestStack $request_stack) {
+    $this->root = $root;
+    $this->batchStorage = $batch_storage;
+    $this->dateFormatter = $date_formatter;
+    $this->formSubmitter = $form_submitter;
+    $this->requestStack = $request_stack;
+
+    $this->batches = [];
+    $this->queues = [];
+  }
+
+  /**
+   * {@inheritdoc}
    */
-  public static function enqueue(Batch $batch) {
+  public function enqueue(Batch $batch) {
     // Initialize the batch if needed.
-    if (empty(self::$batches)) {
-      self::$batches = [
+    if (empty($this->batches)) {
+      $this->batches = [
         'sets' => [],
         'has_form_submits' => FALSE,
       ];
@@ -91,114 +146,94 @@ public static function enqueue(Batch $batch) {
     $batch_set['total'] = count($batch_set['operations']);
     $batch_set['count'] = $batch_set['total'];
     // Add the set to the batch.
-    if (empty(self::$batches['id'])) {
+    if (empty($this->batches['id'])) {
       // The batch is not running yet. Simply add the new set.
-      self::$batches['sets'][] = $batch_set;
+      $this->batches['sets'][] = $batch_set;
     }
     else {
       // The set is being added while the batch is running. Insert the new set
       // right after the current one to ensure execution order, and store its
       // operations in a queue.
-      $index = self::$batches['current_set'] + 1;
-      $slice1 = array_slice(self::$batches['sets'], 0, $index);
-      $slice2 = array_slice(self::$batches['sets'], $index);
-      self::$batches['sets'] = array_merge($slice1, [$batch_set], $slice2);
-      self::populateQueue($index);
+      $index = $this->batches['current_set'] + 1;
+      $slice1 = array_slice($this->batches['sets'], 0, $index);
+      $slice2 = array_slice($this->batches['sets'], $index);
+      $this->batches['sets'] = array_merge($slice1, [$batch_set], $slice2);
+      $this->populateQueue($index);
     }
   }
 
   /**
-   * Processes the batch.
-   *
-   * This function is generally not needed in form submit handlers;
-   * Form API takes care of batches that were set during form submission.
-   *
-   * @param \Drupal\Core\Url|string $redirect
-   *   (optional) Either path or Url object to redirect to when the batch has
-   *   finished processing. Note that to simply force a batch to (conditionally)
-   *   redirect to a custom location after it is finished processing but to
-   *   otherwise allow the standard form API batch handling to occur, it is not
-   *   necessary to call batch_process() and use this parameter. Instead, make
-   *   the batch 'finished' callback return an instance of
-   *   \Symfony\Component\HttpFoundation\RedirectResponse, which will be used
-   *   automatically by the standard batch processing pipeline (and which takes
-   *   precedence over this parameter).
-   * @param \Drupal\Core\Url $url
-   *   (optional) URL of the batch processing page.
-   *   Should only be used for separate scripts like update.php.
-   * @param string $redirect_callback
-   *   (optional) Specify a function to be called to redirect to the progressive
-   *   processing page.
-   *
-   * @return \Symfony\Component\HttpFoundation\RedirectResponse|null
-   *   A redirect response if the batch is progressive. No return value
-   *   otherwise.
+   * {@inheritdoc}
    */
-  public static function process($redirect = NULL, Url $url = NULL, $redirect_callback = NULL) {
-    if (isset(self::$batches)) {
+  public function process($redirect = NULL, Url $url = NULL, $redirect_callback = NULL) {
+    if (isset($this->batches)) {
       // Add process information.
       $process_info = [
         'current_set' => 0,
         'progressive' => TRUE,
         'url' => isset($url) ? $url : Url::fromRoute('system.batch_page.html'),
-        'source_url' => Url::fromRouteMatch(\Drupal::routeMatch())->mergeOptions(['query' => \Drupal::request()->query->all()]),
+        'source_url' => Url::fromRouteMatch(\Drupal::routeMatch())->mergeOptions(
+          [
+            'query' => $this->requestStack->getCurrentRequest()->query->all(),
+          ]
+        ),
         'batch_redirect' => $redirect,
         'theme' => \Drupal::theme()->getActiveTheme()->getName(),
         'redirect_callback' => $redirect_callback,
       ];
-      self::$batches += $process_info;
+      $this->batches += $process_info;
 
       // The batch is now completely built. Allow other modules to make changes
       // to the batch so that it is easier to reuse batch processes in other
       // environments.
-      \Drupal::moduleHandler()->alter('batch', self::$batches);
+      \Drupal::moduleHandler()->alter('batch', $this->batches);
 
       // Assign an arbitrary id: don't rely on a serial column in the 'batch'
       // table, since non-progressive batches skip database storage completely.
-      self::$batches['id'] = Database::getConnection()->nextId();
+      $this->batches['id'] = Database::getConnection()->nextId();
 
       // Move operations to a job queue. Non-progressive batches will use a
       // memory-based queue.
-      foreach (self::$batches['sets'] as $key => $batch_set) {
-        self::populateQueue($key);
+      foreach ($this->batches['sets'] as $key => $batch_set) {
+        $this->populateQueue($key);
       }
 
       // Initiate processing.
-      if (self::$batches['progressive']) {
+      if ($this->batches['progressive']) {
         // Now that we have a batch id, we can generate the redirection link in
         // the generic error message.
         /** @var \Drupal\Core\Url $batch_url */
-        $batch_url = self::$batches['url'];
+        $batch_url = $this->batches['url'];
         /** @var \Drupal\Core\Url $error_url */
         $error_url = clone $batch_url;
         $query_options = $error_url->getOption('query');
-        $query_options['id'] = self::$batches['id'];
+        $query_options['id'] = $this->batches['id'];
         $query_options['op'] = 'finished';
         $error_url->setOption('query', $query_options);
 
-        self::$batches['error_message'] = t('Please continue to <a href=":error_url">the error page</a>', [':error_url' => $error_url->toString(TRUE)->getGeneratedUrl()]);
+        $this->batches['error_message'] = t('Please continue to <a href=":error_url">the error page</a>', [':error_url' => $error_url->toString(TRUE)->getGeneratedUrl()]);
 
         // Clear the way for the redirection to the batch processing page, by
         // saving and un-setting the 'destination', if there is any.
-        $request = \Drupal::request();
+        $request = $this->requestStack->getCurrentRequest();
         if ($request->query->has('destination')) {
-          self::$batches['destination'] = $request->query->get('destination');
+          $this->batches['destination'] = $request->query->get('destination');
           $request->query->remove('destination');
         }
 
         // Store the batch.
-        \Drupal::service('batch.storage')->create(self::$batches);
+        $this->batchStorage->create($this->batches);
 
         // Set the batch number in the session to guarantee that it will stay
         // alive.
-        $_SESSION['batches'][self::$batches['id']] = TRUE;
+        $_SESSION['batches'][$this->batches['id']] = TRUE;
 
         // Redirect for processing.
         $query_options = $error_url->getOption('query');
         $query_options['op'] = 'start';
-        $query_options['id'] = self::$batches['id'];
+        $query_options['id'] = $this->batches['id'];
         $batch_url->setOption('query', $query_options);
-        if (($function = self::$batches['redirect_callback']) && function_exists($function)) {
+        if (($function = $this->batches['redirect_callback']) && function_exists($function)) {
           $function($batch_url->toString(), ['query' => $query_options]);
         }
         else {
@@ -206,9 +241,9 @@ public static function process($redirect = NULL, Url $url = NULL, $redirect_call
         }
       }
       else {
-        // Non-progressive execution: bypass the whole progressbar workflow
+        // Non-progressive execution:  bypass the whole progressbar workflow
         // and execute the batch in one pass.
-        self::processQueue();
+        $this->processQueue();
       }
     }
 
@@ -216,62 +251,41 @@ public static function process($redirect = NULL, Url $url = NULL, $redirect_call
   }
 
   /**
-   * Retrieves the current batch.
+   * {@inheritdoc}
    */
-  public static function &getBatches() {
-    return self::$batches;
+  public function &getBatches() {
+    return $this->batches;
   }
 
   /**
-   * Returns a queue object for a batch set.
-   *
-   * @param array $batch
-   *   The batch set.
-   *
-   * @return \Drupal\Core\Queue\QueueInterface|null
-   *   The queue object or null if the queue cannot be created.
+   * {@inheritdoc}
    */
-  public static function getQueueForBatch($batch) {
+  public function getQueueForBatch($batch) {
     if (isset($batch['queue'])) {
-      return self::getQueue($batch['queue']['name'], $batch['queue']['class']);
+      return $this->getQueue($batch['queue']['name'], $batch['queue']['class']);
     }
     return NULL;
   }
 
   /**
-   * Returns the batch set being currently processed.
-   *
-   * @return array
-   *   The current batch set.
-   *
-   * @internal
+   * {@inheritdoc}
    */
-  public static function &getCurrentSet() {
-    return self::$batches['sets'][self::$batches['current_set']];
+  public function &getCurrentSet() {
+    return $this->batches['sets'][$this->batches['current_set']];
   }
 
   /**
-   * Retrieves the next set in a batch.
-   *
-   * If there is a subsequent set in this batch, assign it as the new set to
-   * process and execute its form submit handler (if defined), which may add
-   * further sets to this batch.
-   *
-   * @return bool
-   *   TRUE if a subsequent set was found in the batch; FALSE will be returned
-   *   if no subsequent set was found.
-   *
-   * @internal
+   * {@inheritdoc}
    */
-  public static function nextSet() {
-    if (isset(self::$batches['sets'][self::$batches['current_set'] + 1])) {
-      self::$batches['current_set']++;
-      $current_set = &self::getCurrentSet();
+  public function nextSet() {
+    if (isset($this->batches['sets'][$this->batches['current_set'] + 1])) {
+      $this->batches['current_set']++;
+      $current_set = &$this->getCurrentSet();
       if (isset($current_set['form_submit']) && ($callback = $current_set['form_submit']) && is_callable($callback)) {
         // We use our stored copies of $form and $form_state to account for
         // possible alterations by previous form submit handlers.
-        $complete_form = &self::$batches['form_state']->getCompleteForm();
-        call_user_func_array($callback, [$complete_form, self::$batches['form_state']]);
+        $complete_form = &$this->batches['form_state']->getCompleteForm();
+        call_user_func_array($callback, [$complete_form, $this->batches['form_state']]);
       }
       return TRUE;
     }
@@ -279,20 +293,13 @@ public static function nextSet() {
   }
 
   /**
-   * Processes sets in a batch.
-   *
-   * If the batch was marked for progressive execution (default), this executes
-   * as many operations in batch sets until an execution time of 1 second has
-   * been exceeded. It will continue with the next operation of the same batch
-   * set in the next request.
-   *
-   * @return array
-   *   An array containing a completion value (in percent) and a status message.
-   *
-   * @internal
+   * {@inheritdoc}
    */
-  public static function processQueue() {
-    $current_set = &self::getCurrentSet();
+  public function processQueue() {
+    $old_set = [];
+    $finished = 1;
+    $label = '';
+    $current_set = &$this->getCurrentSet();
     // Indicate that this batch set needs to be initialized.
     $set_changed = TRUE;
 
@@ -300,7 +307,7 @@ public static function processQueue() {
     // by \Drupal::formBuilder()->submitForm(), initialize a timer to determine
     // whether we need to proceed with the same batch phase when a processing
     // time of 1 second has been exceeded.
-    if (self::$batches['progressive']) {
+    if ($this->batches['progressive']) {
       Timer::start('batch_processing');
     }
 
@@ -308,14 +315,14 @@ public static function processQueue() {
       $current_set['start'] = microtime(TRUE);
     }
 
-    $queue = self::getQueueForBatch($current_set);
+    $queue = $this->getQueueForBatch($current_set);
 
     while (!$current_set['success']) {
       // If this is the first time we iterate this batch set in the current
       // request, we check if it requires an additional file for functions
       // definitions.
       if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) {
-        include_once \Drupal::root() . '/' . $current_set['file'];
+        include_once $this->root . '/' . $current_set['file'];
       }
 
       $task_message = $label = '';
@@ -333,7 +340,7 @@ public static function processQueue() {
           'finished' => &$finished,
           'message'  => &$task_message,
         ];
-        call_user_func_array($callback, array_merge($args, [&$batch_context]));
+        call_user_func_array($callback, array_merge($args, [ & $batch_context]));
 
         if ($finished >= 1) {
           // Make sure this step is not counted twice when computing $current.
@@ -353,25 +360,25 @@ public static function processQueue() {
       // new sets to the batch.
       $set_changed = FALSE;
       $old_set = $current_set;
-      while (empty($current_set['count']) && ($current_set['success'] = TRUE) && self::nextSet()) {
-        $current_set = &self::getCurrentSet();
+      while (empty($current_set['count']) && ($current_set['success'] = TRUE) && $this->nextSet()) {
+        $current_set = &$this->getCurrentSet();
         $current_set['start'] = microtime(TRUE);
         $set_changed = TRUE;
       }
 
       // At this point, either $current_set contains operations that need to be
       // processed or all sets have been completed.
-      $queue = self::getQueueForBatch($current_set);
+      $queue = $this->getQueueForBatch($current_set);
 
       // If we are in progressive mode, break processing after 1 second.
-      if (self::$batches['progressive'] && Timer::read('batch_processing') > 1000) {
+      if ($this->batches['progressive'] && Timer::read('batch_processing') > 1000) {
         // Record elapsed wall clock time.
         $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
         break;
       }
     }
 
-    if (self::$batches['progressive']) {
+    if ($this->batches['progressive']) {
       // Gather progress information.
       //
       // Reporting 100% progress will cause the whole batch to be considered
@@ -401,9 +408,9 @@ public static function processQueue() {
         '@total'      => $total,
         '@current'    => floor($current),
         '@percentage' => $percentage,
-        '@elapsed'    => \Drupal::service('date.formatter')->formatInterval($elapsed / 1000),
+        '@elapsed'    => $this->dateFormatter->formatInterval($elapsed / 1000),
         // If possible, estimate remaining processing time.
-        '@estimate'   => ($current > 0) ? \Drupal::service('date.formatter')->formatInterval(($elapsed * ($total - $current) / $current) / 1000) : '-',
+        '@estimate'   => ($current > 0) ? $this->dateFormatter->formatInterval(($elapsed * ($total - $current) / $current) / 1000) : '-',
       ];
       $message = strtr($progress_message, $values);
       if (!empty($task_message)) {
@@ -414,36 +421,26 @@ public static function processQueue() {
     }
     else {
       // If we are not in progressive mode, the entire batch has been processed.
-      return self::finishedProcessing();
+      return $this->finishedProcessing();
     }
   }
 
   /**
-   * Ends the batch processing.
-   *
-   * Call the 'finished' callback of each batch set to allow custom handling of
-   * the results and resolve page redirection.
-   *
-   * @return \Symfony\Component\HttpFoundation\RedirectResponse|false
-   *   A redirect response to the completed page or NULL to stay at the current
-   *   URL.
-   *
-   * @internal
+   * {@inheritdoc}
    */
-  public static function finishedProcessing() {
-    self::$batches = &batch_get();
+  public function finishedProcessing() {
     $batch_finished_redirect = NULL;
 
     // Execute the 'finished' callbacks for each batch set, if defined.
-    foreach (self::$batches['sets'] as $batch_set) {
+    foreach ($this->batches['sets'] as $batch_set) {
       if (isset($batch_set['finished'])) {
         // Check if the set requires an additional file for function
         // definitions.
         if (isset($batch_set['file']) && is_file($batch_set['file'])) {
-          include_once \Drupal::root() . '/' . $batch_set['file'];
+          include_once $this->root . '/' . $batch_set['file'];
         }
         if (is_callable($batch_set['finished'])) {
-          $queue = self::getQueueForBatch($batch_set);
+          $queue = $this->getQueueForBatch($batch_set);
           $operations = $queue->getAllItems();
           $batch_set_result = call_user_func_array(
             $batch_set['finished'],
@@ -451,8 +448,7 @@ public static function finishedProcessing() {
               $batch_set['success'],
               $batch_set['results'],
               $operations,
-              \Drupal::service('date.formatter')
-                ->formatInterval($batch_set['elapsed'] / 1000),
+              $this->dateFormatter->formatInterval($batch_set['elapsed'] / 1000),
             ]
           );
           // If a batch 'finished' callback requested a redirect after the batch
@@ -466,29 +462,29 @@ public static function finishedProcessing() {
     }
 
     // Clean up the batch table and unset the static $batch variable.
-    if (self::$batches['progressive']) {
-      \Drupal::service('batch.storage')->delete(self::$batches['id']);
-      foreach (self::$batches['sets'] as $batch_set) {
-        if ($queue = self::getQueueForBatch($batch_set)) {
+    if ($this->batches['progressive']) {
+      $this->batchStorage->delete($this->batches['id']);
+      foreach ($this->batches['sets'] as $batch_set) {
+        if ($queue = $this->getQueueForBatch($batch_set)) {
           $queue->deleteQueue();
         }
       }
       // Clean-up the session. Not needed for CLI updates.
       if (isset($_SESSION)) {
-        unset($_SESSION['batches'][self::$batches['id']]);
+        unset($_SESSION['batches'][$this->batches['id']]);
         if (empty($_SESSION['batches'])) {
           unset($_SESSION['batches']);
         }
       }
     }
-    $_batch = self::$batches;
-    self::$batches = NULL;
+    $_batch = $this->batches;
+    $this->batches = NULL;
 
     // Redirect if needed.
     if ($_batch['progressive']) {
       // Revert the 'destination' that was saved in batch_process().
       if (isset($_batch['destination'])) {
-        \Drupal::request()->query->set('destination', $_batch['destination']);
+        $this->requestStack->getCurrentRequest()->query->set('destination', $_batch['destination']);
       }
 
       // Determine the target path to redirect to. If a batch 'finished'
@@ -522,7 +518,7 @@ public static function finishedProcessing() {
 
       // Use \Drupal\Core\Form\FormSubmitterInterface::redirectForm() to handle
       // the redirection logic.
-      $redirect = \Drupal::service('form_submitter')->redirectForm($_batch['form_state']);
+      $redirect = $this->formSubmitter->redirectForm($_batch['form_state']);
       if (is_object($redirect)) {
         return $redirect;
       }
@@ -559,11 +555,11 @@ public static function finishedProcessing() {
    * @return \Drupal\Core\Queue\QueueInterface
    *   The queue object.
    */
-  protected static function getQueue($name, $class) {
-    if (!isset(self::$queues[$class][$name])) {
-      self::$queues[$class][$name] = new $class($name, \Drupal::database());
+  protected function getQueue($name, $class) {
+    if (!isset($this->queues[$class][$name])) {
+      $this->queues[$class][$name] = new $class($name, Database::getConnection());
     }
-    return self::$queues[$class][$name];
+    return $this->queues[$class][$name];
   }
 
   /**
@@ -577,8 +573,8 @@ protected static function getQueue($name, $class) {
    * @param int $set_id
    *   The id of the set to process.
    */
-  protected static function populateQueue($set_id = 0) {
-    $batch = &self::$batches;
+  protected function populateQueue($set_id = 0) {
+    $batch = &$this->batches;
     $batch_set = &$batch['sets'][$set_id];
 
     if (isset($batch_set['operations'])) {
@@ -591,7 +587,7 @@ protected static function populateQueue($set_id = 0) {
         ];
       }
 
-      $queue = self::getQueueForBatch($batch_set);
+      $queue = $this->getQueueForBatch($batch_set);
       $queue->createQueue();
       foreach ($batch_set['operations'] as $operation) {
         $queue->createItem($operation);
diff --git a/core/lib/Drupal/Core/Batch/BatchQueueControllerInterface.php b/core/lib/Drupal/Core/Batch/BatchQueueControllerInterface.php
new file mode 100644
index 0000000000..6a858a5658
--- /dev/null
+++ b/core/lib/Drupal/Core/Batch/BatchQueueControllerInterface.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Drupal\Core\Batch;
+
+use Drupal\Core\Url;
+
+interface BatchQueueControllerInterface {
+
+  /**
+   * Places the batch in the queue to be processed.
+   *
+   * Batch objects are also queued by invoking <code>enqueue()</code> directly
+   * on the object.
+   *
+   * @param \Drupal\Core\Batch\Batch $batch
+   *   The batch to place in the queue.
+   *
+   * @see \Drupal\Core\Batch\Batch::enqueue()
+   */
+  public function enqueue(Batch $batch);
+
+  /**
+   * Processes the batch.
+   *
+   * This function is generally not needed in form submit handlers;
+   * Form API takes care of batches that were set during form submission.
+   *
+   * @param \Drupal\Core\Url|string $redirect
+   *   (optional) Either path or Url object to redirect to when the batch has
+   *   finished processing. Note that to simply force a batch to (conditionally)
+   *   redirect to a custom location after it is finished processing but to
+   *   otherwise allow the standard form API batch handling to occur, it is not
+   *   necessary to call batch_process() and use this parameter. Instead, make
+   *   the batch 'finished' callback return an instance of
+   *   \Symfony\Component\HttpFoundation\RedirectResponse, which will be used
+   *   automatically by the standard batch processing pipeline (and which takes
+   *   precedence over this parameter).
+   * @param \Drupal\Core\Url $url
+   *   (optional) URL of the batch processing page.
+   *   Should only be used for separate scripts like update.php.
+   * @param string $redirect_callback
+   *   (optional) Specify a function to be called to redirect to the progressive
+   *   processing page.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse|null
+   *   A redirect response if the batch is progressive. No return value
+   *   otherwise.
+   */
+  public function process($redirect = NULL, Url $url = NULL, $redirect_callback = NULL);
+
+  /**
+   * Retrieves the current batch.
+   */
+  public function &getBatches();
+
+  /**
+   * Returns a queue object for a batch set.
+   *
+   * @param array $batch
+   *   The batch set.
+   *
+   * @return \Drupal\Core\Queue\QueueInterface|null
+   *   The queue object or null if the queue cannot be created.
+   */
+  public function getQueueForBatch($batch);
+
+  /**
+   * Returns the batch set being currently processed.
+   *
+   * @return array
+   *   The current batch set.
+   *
+   * @internal This is an internal function and will be removed. Use
+   *   _batch_current_set() instead.
+   */
+  public function &getCurrentSet();
+
+
+  /**
+   * Retrieves the next set in a batch.
+   *
+   * If there is a subsequent set in this batch, assign it as the new set to
+   * process and execute its form submit handler (if defined), which may add
+   * further sets to this batch.
+   *
+   * @return bool
+   *   TRUE if a subsequent set was found in the batch; FALSE will be returned
+   *   if no subsequent set was found.
+   *
+   * @internal This is an internal function and will be removed. Use
+   *   _batch_next_set() instead.
+   */
+  public function nextSet();
+
+  /**
+   * Processes sets in a batch.
+   *
+   * If the batch was marked for progressive execution (default), this executes
+   * as many operations in batch sets until an execution time of 1 second has
+   * been exceeded. It will continue with the next operation of the same batch
+   * set in the next request.
+   *
+   * @return array
+   *   An array containing a completion value (in percent) and a status message.
+   *
+   * @internal This is an internal function and will be removed. Use
+   *   _batch_process() instead.
+   */
+  public function processQueue();
+
+  /**
+   * Ends the batch processing.
+   *
+   * Call the 'finished' callback of each batch set to allow custom handling of
+   * the results and resolve page redirection.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse|false
+   *   A redirect response to the completed page or NULL to stay at the current
+   *   URL.
+   *
+   * @internal This is an internal function and will be removed. Use
+   *   _batch_finished() instead.
+   */
+  public function finishedProcessing();
+
+}
