diff --git a/core/core.services.yml b/core/core.services.yml
index 9d40627..3bfea9a 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1237,6 +1237,11 @@ services:
     tags:
       - { name: backend_overridable }
     lazy: true
+  batch.processor:
+    class: Drupal\Core\Batch\BatchProcessor
+    arguments: ['@app.root', '@batch.queue', '@date.formatter', '@form_submitter', '@batch.storage', '@request_stack']
+  batch.queue:
+    class: Drupal\Core\Batch\BatchQueueHandler
   replica_database_ignore__subscriber:
     class: Drupal\Core\EventSubscriber\ReplicaDatabaseIgnoreSubscriber
     tags:
diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index cc70557..c8db02a 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -15,11 +15,6 @@
  */
 
 use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Component\Utility\Timer;
-use Drupal\Component\Utility\UrlHelper;
-use Drupal\Core\Batch\Percentage;
-use Drupal\Core\Form\FormState;
-use Drupal\Core\Url;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -69,13 +64,15 @@ function _batch_page(Request $request) {
     }
   }
 
+  /** @var \Drupal\Core\Batch\BatchProcessorInterface */
+  $batch_processor = \Drupal::service('batch.processor');
   $op = $request->get('op', '');
   switch ($op) {
     case 'start':
     case 'do_nojs':
       // Display the full progress page on startup and on each additional
       // non-JavaScript iteration.
-      $current_set = _batch_current_set();
+      $current_set = $batch_processor->currentSet();
       $build['#title'] = $current_set['title'];
       $build['content'] = _batch_progress_page();
       break;
@@ -85,8 +82,9 @@ function _batch_page(Request $request) {
       return _batch_do();
 
     case 'finished':
-      // _batch_finished() returns a RedirectResponse.
-      return _batch_finished();
+      // \Drupal\Core\Batch\BatchProcessorInterface::finished() returns a
+      // RedirectResponse.
+      return $batch_processor->finished();
   }
 
   return $build;
@@ -96,11 +94,13 @@ function _batch_page(Request $request) {
  * Does one execution pass with JavaScript and returns progress to the browser.
  *
  * @see _batch_progress_page_js()
- * @see _batch_process()
+ * @see \Drupal\Core\Batch\BatchProcessorInterface::processBatchSet()
  */
 function _batch_do() {
   // Perform actual processing.
-  list($percentage, $message, $label) = _batch_process();
+  /** @var \Drupal\Core\Batch\BatchProcessorInterface */
+  $batch_processor = \Drupal::service('batch.processor');
+  list($percentage, $message, $label) = $batch_processor->processBatchSet();
 
   return new JsonResponse(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message, 'label' => $label));
 }
@@ -108,12 +108,14 @@ function _batch_do() {
 /**
  * Outputs a batch processing page.
  *
- * @see _batch_process()
+ * @see \Drupal\Core\Batch\BatchProcessorInterface::processBatchSet()
  */
 function _batch_progress_page() {
   $batch = &batch_get();
 
-  $current_set = _batch_current_set();
+  /** @var \Drupal\Core\Batch\BatchProcessorInterface */
+  $batch_processor = \Drupal::service('batch.processor');
+  $current_set = $batch_processor->currentSet();
 
   $new_op = 'do_nojs';
 
@@ -149,7 +151,7 @@ function _batch_progress_page() {
     print $fallback;
 
     // Perform actual processing.
-    list($percentage, $message, $label) = _batch_process($batch);
+    list($percentage, $message, $label) = $batch_processor->processBatchSet($batch);
     if ($percentage == 100) {
       $new_op = 'finished';
     }
@@ -204,316 +206,6 @@ function _batch_progress_page() {
 }
 
 /**
- * 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
- *   An array containing a completion value (in percent) and a status message.
- */
-function _batch_process() {
-  $batch       = &batch_get();
-  $current_set = &_batch_current_set();
-  // Indicate that this batch set needs to be initialized.
-  $set_changed = TRUE;
-
-  // If this batch was marked for progressive execution (e.g. forms submitted 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 ($batch['progressive']) {
-    Timer::start('batch_processing');
-  }
-
-  if (empty($current_set['start'])) {
-    $current_set['start'] = microtime(TRUE);
-  }
-
-  $queue = _batch_queue($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'];
-    }
-
-    $task_message = $label = '';
-    // Assume a single pass operation and set the completion level to 1 by
-    // default.
-    $finished = 1;
-
-    if ($item = $queue->claimItem()) {
-      list($callback, $args) = $item->data;
-
-      // Build the 'context' array and execute the function call.
-      $batch_context = array(
-        'sandbox'  => &$current_set['sandbox'],
-        'results'  => &$current_set['results'],
-        'finished' => &$finished,
-        'message'  => &$task_message,
-      );
-      call_user_func_array($callback, array_merge($args, array(&$batch_context)));
-
-      if ($finished >= 1) {
-        // Make sure this step is not counted twice when computing $current.
-        $finished = 0;
-        // Remove the processed operation and clear the sandbox.
-        $queue->deleteItem($item);
-        $current_set['count']--;
-        $current_set['sandbox'] = array();
-      }
-    }
-
-    // When all operations in the current batch set are completed, browse
-    // through the remaining sets, marking them 'successfully processed'
-    // along the way, until we find a set that contains operations.
-    // _batch_next_set() executes form submit handlers stored in 'control'
-    // sets (see \Drupal::service('form_submitter')), which can in turn add new
-    // sets to the batch.
-    $set_changed = FALSE;
-    $old_set = $current_set;
-    while (empty($current_set['count']) && ($current_set['success'] = TRUE) && _batch_next_set()) {
-      $current_set = &_batch_current_set();
-      $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 = _batch_queue($current_set);
-
-    // If we are in progressive mode, break processing after 1 second.
-    if ($batch['progressive'] && Timer::read('batch_processing') > 1000) {
-      // Record elapsed wall clock time.
-      $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
-      break;
-    }
-  }
-
-  if ($batch['progressive']) {
-    // Gather progress information.
-
-    // Reporting 100% progress will cause the whole batch to be considered
-    // processed. If processing was paused right after moving to a new set,
-    // we have to use the info from the new (unprocessed) set.
-    if ($set_changed && isset($current_set['queue'])) {
-      // Processing will continue with a fresh batch set.
-      $remaining        = $current_set['count'];
-      $total            = $current_set['total'];
-      $progress_message = $current_set['init_message'];
-      $task_message     = '';
-    }
-    else {
-      // Processing will continue with the current batch set.
-      $remaining        = $old_set['count'];
-      $total            = $old_set['total'];
-      $progress_message = $old_set['progress_message'];
-    }
-
-    // Total progress is the number of operations that have fully run plus the
-    // completion level of the current operation.
-    $current    = $total - $remaining + $finished;
-    $percentage = _batch_api_percentage($total, $current);
-    $elapsed    = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0;
-    $values     = array(
-      '@remaining'  => $remaining,
-      '@total'      => $total,
-      '@current'    => floor($current),
-      '@percentage' => $percentage,
-      '@elapsed'    => \Drupal::service('date.formatter')->formatInterval($elapsed / 1000),
-      // If possible, estimate remaining processing time.
-      '@estimate'   => ($current > 0) ? \Drupal::service('date.formatter')->formatInterval(($elapsed * ($total - $current) / $current) / 1000) : '-',
-    );
-    $message = strtr($progress_message, $values);
-    if (!empty($task_message)) {
-      $label = $task_message;
-    }
-
-    return array($percentage, $message, $label);
-  }
-  else {
-    // If we are not in progressive mode, the entire batch has been processed.
-    return _batch_finished();
-  }
-}
-
-/**
- * Formats the percent completion for a batch set.
- *
- * @param $total
- *   The total number of operations.
- * @param $current
- *   The number of the current operation. This may be a floating point number
- *   rather than an integer in the case of a multi-step operation that is not
- *   yet complete; in that case, the fractional part of $current represents the
- *   fraction of the operation that has been completed.
- *
- * @return
- *   The properly formatted percentage, as a string. We output percentages
- *   using the correct number of decimal places so that we never print "100%"
- *   until we are finished, but we also never print more decimal places than
- *   are meaningful.
- *
- * @see _batch_process()
- */
-function _batch_api_percentage($total, $current) {
-  return Percentage::format($total, $current);
-}
-
-/**
- * Returns the batch set being currently processed.
- */
-function &_batch_current_set() {
-  $batch = &batch_get();
-  return $batch['sets'][$batch['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
- *   TRUE if a subsequent set was found in the batch.
- */
-function _batch_next_set() {
-  $batch = &batch_get();
-  if (isset($batch['sets'][$batch['current_set'] + 1])) {
-    $batch['current_set']++;
-    $current_set = &_batch_current_set();
-    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 = &$batch['form_state']->getCompleteForm();
-      call_user_func_array($callback, array(&$complete_form, &$batch['form_state']));
-    }
-    return TRUE;
-  }
-}
-
-/**
- * Ends the batch processing.
- *
- * Call the 'finished' callback of each batch set to allow custom handling of
- * the results and resolve page redirection.
- */
-function _batch_finished() {
-  $batch = &batch_get();
-  $batch_finished_redirect = NULL;
-
-  // Execute the 'finished' callbacks for each batch set, if defined.
-  foreach ($batch['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'];
-      }
-      if (is_callable($batch_set['finished'])) {
-        $queue = _batch_queue($batch_set);
-        $operations = $queue->getAllItems();
-        $batch_set_result = call_user_func_array($batch_set['finished'], array($batch_set['success'], $batch_set['results'], $operations, \Drupal::service('date.formatter')->formatInterval($batch_set['elapsed'] / 1000)));
-        // If a batch 'finished' callback requested a redirect after the batch
-        // is complete, save that for later use. If more than one batch set
-        // returned a redirect, the last one is used.
-        if ($batch_set_result instanceof RedirectResponse) {
-          $batch_finished_redirect = $batch_set_result;
-        }
-      }
-    }
-  }
-
-  // Clean up the batch table and unset the static $batch variable.
-  if ($batch['progressive']) {
-    \Drupal::service('batch.storage')->delete($batch['id']);
-    foreach ($batch['sets'] as $batch_set) {
-      if ($queue = _batch_queue($batch_set)) {
-        $queue->deleteQueue();
-      }
-    }
-    // Clean-up the session. Not needed for CLI updates.
-    if (isset($_SESSION)) {
-      unset($_SESSION['batches'][$batch['id']]);
-      if (empty($_SESSION['batches'])) {
-        unset($_SESSION['batches']);
-      }
-    }
-  }
-  $_batch = $batch;
-  $batch = 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']);
-    }
-
-    // Determine the target path to redirect to. If a batch 'finished' callback
-    // returned a redirect response object, use that. Otherwise, fall back on
-    // the form redirection.
-    if (isset($batch_finished_redirect)) {
-      return $batch_finished_redirect;
-    }
-    elseif (!isset($_batch['form_state'])) {
-      $_batch['form_state'] = new FormState();
-    }
-    if ($_batch['form_state']->getRedirect() === NULL) {
-      $redirect = $_batch['batch_redirect'] ?: $_batch['source_url'];
-      // Any path with a scheme does not correspond to a route.
-      if (!$redirect instanceof Url) {
-        $options = UrlHelper::parse($redirect);
-        if (parse_url($options['path'], PHP_URL_SCHEME)) {
-          $redirect = Url::fromUri($options['path'], $options);
-        }
-        else {
-          $redirect = \Drupal::pathValidator()->getUrlIfValid($options['path']);
-          if (!$redirect) {
-            // Stay on the same page if the redirect was invalid.
-            $redirect = Url::fromRoute('<current>');
-          }
-          $redirect->setOptions($options);
-        }
-      }
-      $_batch['form_state']->setRedirectUrl($redirect);
-    }
-
-    // Use \Drupal\Core\Form\FormSubmitterInterface::redirectForm() to handle
-    // the redirection logic.
-    $redirect = \Drupal::service('form_submitter')->redirectForm($_batch['form_state']);
-    if (is_object($redirect)) {
-      return $redirect;
-    }
-
-    // If no redirection happened, redirect to the originating page. In case the
-    // form needs to be rebuilt, save the final $form_state for
-    // \Drupal\Core\Form\FormBuilderInterface::buildForm().
-    if ($_batch['form_state']->isRebuilding()) {
-      $_SESSION['batch_form_state'] = $_batch['form_state'];
-    }
-    $callback = $_batch['redirect_callback'];
-    /** @var \Drupal\Core\Url $source_url */
-    $source_url = $_batch['source_url'];
-    if (is_callable($callback)) {
-      $callback($_batch['source_url'], array('query' => array('op' => 'finish', 'id' => $_batch['id'])));
-    }
-    elseif ($callback === NULL) {
-      // Default to RedirectResponse objects when nothing specified.
-      $url = $source_url
-        ->setAbsolute()
-        ->setOption('query', ['op' => 'finish', 'id' => $_batch['id']]);
-      return new RedirectResponse($url->toString());
-    }
-  }
-}
-
-/**
  * Shutdown function: Stores the current batch data for the next request.
  *
  * @see _batch_page()
diff --git a/core/includes/form.inc b/core/includes/form.inc
index efbac99..945f2b4 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -875,7 +875,9 @@ function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = N
       // Non-progressive execution: bypass the whole progressbar workflow
       // and execute the batch in one pass.
       require_once __DIR__ . '/batch.inc';
-      _batch_process();
+      /** @var \Drupal\Core\Batch\BatchProcessorInterface $batch_processor */
+      $batch_processor = \Drupal::service('batch.processor');
+      $batch_processor->processBatchSet();
     }
   }
 }
@@ -884,14 +886,9 @@ function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = N
  * 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.
-  static $batch = array();
-  return $batch;
+  /** @var \Drupal\Core\Batch\BatchProcessorInterface $batch_processor */
+  $batch_processor = \Drupal::service('batch.processor');
+  return $batch_processor->getBatch();
 }
 
 /**
@@ -908,26 +905,12 @@ function &batch_get() {
  *
  * @return
  *   The name and class of the queue are added by reference to the batch set.
+ *
+ * @deprecated Use \Drupal::service('batch.queue')->populateQueue($batch, $set_id)
+ *   instead.
  */
 function _batch_populate_queue(&$batch, $set_id) {
-  $batch_set = &$batch['sets'][$set_id];
-
-  if (isset($batch_set['operations'])) {
-    $batch_set += array(
-      'queue' => array(
-        'name' => 'drupal_batch:' . $batch['id'] . ':' . $set_id,
-        'class' => $batch['progressive'] ? 'Drupal\Core\Queue\Batch' : 'Drupal\Core\Queue\BatchMemory',
-      ),
-    );
-
-    $queue = _batch_queue($batch_set);
-    $queue->createQueue();
-    foreach ($batch_set['operations'] as $operation) {
-      $queue->createItem($operation);
-    }
-
-    unset($batch_set['operations']);
-  }
+  return \Drupal::service('batch.queue')->populateQueue($batch, $set_id);
 }
 
 /**
@@ -938,23 +921,11 @@ function _batch_populate_queue(&$batch, $set_id) {
  *
  * @return
  *   The queue object.
+ *
+ * @deprecated Use \Drupal::service('batch.queue')->getQueue($batch_set) instead.
  */
 function _batch_queue($batch_set) {
-  static $queues;
-
-  if (!isset($queues)) {
-    $queues = array();
-  }
-
-  if (isset($batch_set['queue'])) {
-    $name = $batch_set['queue']['name'];
-    $class = $batch_set['queue']['class'];
-
-    if (!isset($queues[$class][$name])) {
-      $queues[$class][$name] = new $class($name, \Drupal::database());
-    }
-    return $queues[$class][$name];
-  }
+  return \Drupal::service('batch.queue')->getQueue($batch_set);
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Batch/BatchProcessor.php b/core/lib/Drupal/Core/Batch/BatchProcessor.php
new file mode 100644
index 0000000..29319cb
--- /dev/null
+++ b/core/lib/Drupal/Core/Batch/BatchProcessor.php
@@ -0,0 +1,391 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\Batch\BatchProcessor
+ */
+
+namespace Drupal\Core\Batch;
+
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\Timer;
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Datetime\DateFormatter;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormSubmitterInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Theme\ThemeManagerInterface;
+use Drupal\Core\Url;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Provides a batch processor.
+ */
+class BatchProcessor implements BatchProcessorInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The set of batches.
+   *
+   * @var array
+   */
+  protected $batch;
+
+  /**
+   * The batch queue.
+   *
+   * @var \Drupal\Core\Batch\BatchQueueHandlerInterface
+   */
+  protected $batchQueue;
+
+  /**
+   * The date formatter used to calculate the needed time for the batch.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatter
+   */
+  protected $dateFormatter;
+
+  /**
+   * The form submitter used to redirect at the end of the batch.
+   *
+   * @var \Drupal\Core\Form\FormSubmitterInterface
+   */
+  protected $formSubmitter;
+
+  /**
+   * The batch storage service.
+   *
+   * @var \Drupal\Core\Batch\BatchStorageInterface
+   */
+  protected $batchStorage;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The app root.
+   *
+   * @var string
+   */
+  protected $root;
+
+  /**
+   * Creates a new BatchProcessor.
+   *
+   * @param string $root
+   *   The app root.
+   * @param \Drupal\Core\Batch\BatchQueueHandlerInterface $batch_queue
+   *   The batch queue.
+   * @param \Drupal\Core\Datetime\DateFormatter $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 \Drupal\Core\Batch\BatchStorageInterface $batch_storage
+   *   The batch storage.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   */
+  public function __construct($root, BatchQueueHandlerInterface $batch_queue, DateFormatter $date_formatter, FormSubmitterInterface $form_submitter, BatchStorageInterface $batch_storage, RequestStack $request_stack) {
+    $this->root = $root;
+    $this->batchQueue = $batch_queue;
+    $this->dateFormatter = $date_formatter;
+    $this->formSubmitter = $form_submitter;
+    $this->batchStorage = $batch_storage;
+    $this->requestStack = $request_stack;
+    $this->batch = ['sets' => []];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processBatchSet() {
+    $current_set = $this->currentSet();
+    // Indicate that this batch set needs to be initialized.
+    $set_changed = TRUE;
+
+    // If this batch was marked for progressive execution (e.g. forms submitted by
+    // drupal_form_submit()), 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 ($this->batch['progressive']) {
+      Timer::start('batch_processing');
+    }
+
+    if (empty($current_set['start'])) {
+      $current_set['start'] = microtime(TRUE);
+    }
+
+    $queue = $this->batchQueue->getQueue($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'];
+      }
+
+      $task_message = $label = '';
+      // Assume a single pass operation and set the completion level to 1 by
+      // default.
+      $finished = 1;
+
+      if ($item = $queue->claimItem()) {
+        list($callback, $args) = $item->data;
+
+        // Build the 'context' array and execute the function call.
+        $batch_context = [
+          'sandbox'  => &$current_set['sandbox'],
+          'results'  => &$current_set['results'],
+          'finished' => &$finished,
+          'message'  => &$task_message,
+        ];
+        call_user_func_array($callback, array_merge($args, [&$batch_context]));
+
+        if ($finished >= 1) {
+          // Make sure this step is not counted twice when computing $current.
+          $finished = 0;
+          // Remove the processed operation and clear the sandbox.
+          $queue->deleteItem($item);
+          $current_set['count']--;
+          $current_set['sandbox'] = [];
+        }
+      }
+
+      // When all operations in the current batch set are completed, browse
+      // through the remaining sets, marking them 'successfully processed'
+      // along the way, until we find a set that contains operations.
+      // nextSet() executes form submit handlers stored in 'control'
+      // sets (see form_execute_handlers()), which can in turn add new sets to
+      // the batch.
+      $set_changed = FALSE;
+      $old_set = $current_set;
+      while (empty($current_set['count']) && ($current_set['success'] = TRUE) && $this->nextSet()) {
+        $current_set = $this->currentSet();
+        $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 = $this->batchQueue->getQueue($current_set);
+
+      // If we are in progressive mode, break processing after 1 second.
+      if ($this->batch['progressive'] && Timer::read('batch_processing') > 1000) {
+        // Record elapsed wall clock time.
+        $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
+        break;
+      }
+    }
+
+    if ($this->batch['progressive']) {
+      // Gather progress information.
+
+      // Reporting 100% progress will cause the whole batch to be considered
+      // processed. If processing was paused right after moving to a new set,
+      // we have to use the info from the new (unprocessed) set.
+      if ($set_changed && isset($current_set['queue'])) {
+        // Processing will continue with a fresh batch set.
+        $remaining        = $current_set['count'];
+        $total            = $current_set['total'];
+        $progress_message = $current_set['init_message'];
+        $task_message     = '';
+      }
+      else {
+        // Processing will continue with the current batch set.
+        $remaining        = $old_set['count'];
+        $total            = $old_set['total'];
+        $progress_message = $old_set['progress_message'];
+      }
+
+      // Total progress is the number of operations that have fully run plus the
+      // completion level of the current operation.
+      $current    = $total - $remaining + $finished;
+      $percentage = Percentage::format($total, $current);;
+      $elapsed    = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0;
+      $values     = array(
+        '@remaining'  => $remaining,
+        '@total'      => $total,
+        '@current'    => floor($current),
+        '@percentage' => $percentage,
+        '@elapsed'    => $this->dateFormatter->formatInterval($elapsed / 1000),
+        // If possible, estimate remaining processing time.
+        '@estimate'   => ($current > 0) ? $this->dateFormatter->formatInterval(($elapsed * ($total - $current) / $current) / 1000) : '-',
+      );
+      $message = strtr($progress_message, $values);
+      if (!empty($task_message)) {
+        $label = $task_message;
+      }
+
+      return [$percentage, $message, $label];
+    }
+    else {
+      // If we are not in progressive mode, the entire batch has been processed.
+      return $this->finished();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function currentSet() {
+    return $this->batch['sets'][$this->batch['current_set']];
+  }
+
+  /**
+   * @return array
+   */
+  public function &getBatch() {
+    return $this->batch;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function nextSet() {
+    if (isset($this->batch['sets'][$this->batch['current_set'] + 1])) {
+      $this->batch['current_set']++;
+      $current_set = &$this->currentSet();
+      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 = &$this->batch['form_state']->getCompleteForm();
+        call_user_func_array($callback, [&$complete_form, &$this->batch['form_state']]);
+      }
+      return TRUE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function finished() {
+    // Execute the 'finished' callbacks for each batch set, if defined.
+    foreach ($this->batch['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 $this->root . '/' . $batch_set['file'];
+        }
+        if (is_callable($batch_set['finished'])) {
+          $queue = $this->batchQueue->getQueue($batch_set);
+          $operations = $queue->getAllItems();
+          $batch_set_result = call_user_func_array($batch_set['finished'], array($batch_set['success'], $batch_set['results'], $operations, $this->dateFormatter->formatInterval($batch_set['elapsed'] / 1000)));
+          // If a batch 'finished' callback requested a redirect after the batch
+          // is complete, save that for later use. If more than one batch set
+          // returned a redirect, the last one is used.
+          if ($batch_set_result instanceof RedirectResponse) {
+            $batch_finished_redirect = $batch_set_result;
+          }
+        }
+      }
+    }
+
+    // Clean up the batch table and unset the static $this->batch variable.
+    if ($this->batch['progressive']) {
+      $this->batchStorage->delete($this->batch['id']);
+      foreach ($this->batch['sets'] as $batch_set) {
+        if ($queue = $this->batchQueue->getQueue($batch_set)) {
+          $queue->deleteQueue();
+        }
+      }
+      // Clean-up the session. Not needed for CLI updates.
+      if (isset($_SESSION)) {
+        unset($_SESSION['batches'][$this->batch['id']]);
+        if (empty($_SESSION['batches'])) {
+          unset($_SESSION['batches']);
+        }
+      }
+    }
+    $_batch = $this->batch;
+    $this->batch = NULL;
+
+    // Redirect if needed.
+    if ($_batch['progressive']) {
+      // Revert the 'destination' that was saved in batch_process().
+      if (isset($_batch['destination'])) {
+        $this->requestStack->getCurrentRequest()->query->set('destination', $_batch['destination']);
+      }
+
+      // Determine the target path to redirect to.
+      if (!isset($_batch['form_state'])) {
+        $_batch['form_state'] = new FormState();
+      }
+      if ($_batch['form_state']->getRedirect() === NULL) {
+        $redirect = $_batch['batch_redirect'] ?: $_batch['source_url'];
+        // Any path with a scheme does not correspond to a route.
+        if (!$redirect instanceof Url) {
+          $options = UrlHelper::parse($redirect);
+          if (parse_url($options['path'], PHP_URL_SCHEME)) {
+            $redirect = Url::fromUri($options['path'], $options);
+          }
+          else {
+            $redirect = \Drupal::pathValidator()->getUrlIfValid($options['path']);
+            if (!$redirect) {
+              // Stay on the same page if the redirect was invalid.
+              $redirect = Url::fromRoute('<current>');
+            }
+            $redirect->setOptions($options);
+          }
+        }
+        $_batch['form_state']->setRedirectUrl($redirect);
+      }
+
+      // Use \Drupal\Core\Form\FormSubmitterInterface::redirectForm() to handle
+      // the redirection logic.
+      $redirect = $this->formSubmitter->redirectForm($_batch['form_state']);
+      if (is_object($redirect)) {
+        return $redirect;
+      }
+
+      // If no redirection happened, redirect to the originating page. In case the
+      // form needs to be rebuilt, save the final $form_state for
+      // \Drupal\Core\Form\FormBuilderInterface::buildForm().
+      if (!empty($_batch['form_state']['rebuild'])) {
+        $_SESSION['batch_form_state'] = $_batch['form_state'];
+      }
+      $callback = $_batch['redirect_callback'];
+      /** @var \Drupal\Core\Url $source_url */
+      $source_url = $_batch['source_url'];
+      if (is_callable($callback)) {
+        $callback($_batch['source_url'], ['query' => ['op' => 'finish', 'id' => $_batch['id']]]);
+      }
+      elseif ($callback === NULL) {
+        // Default to RedirectResponse objects when nothing specified.
+        $url = $source_url
+          ->setAbsolute()
+          ->setOption('query', ['op' => 'finish', 'id' => $_batch['id']]);
+        return new RedirectResponse($url->toString());
+      }
+    }
+  }
+
+  /**
+   * @return mixed
+   */
+  protected function getBatchId() {
+    return Database::getConnection()->nextId();
+  }
+
+  /**
+   * @param $error_url
+   * @return string
+   */
+  protected function getErrorMessage(Url $error_url) {
+    return $this->t('Please continue to <a href="@error_url">the error page</a>', array(
+      '@error_url' => $error_url->toString(TRUE)
+        ->getGeneratedUrl()
+    ));
+  }
+
+
+}
diff --git a/core/lib/Drupal/Core/Batch/BatchProcessorInterface.php b/core/lib/Drupal/Core/Batch/BatchProcessorInterface.php
new file mode 100644
index 0000000..145c30a
--- /dev/null
+++ b/core/lib/Drupal/Core/Batch/BatchProcessorInterface.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\Batch\BatchProcessorInterface
+ */
+
+namespace Drupal\Core\Batch;
+
+/**
+ * Processes batch requests.
+ */
+interface BatchProcessorInterface {
+
+  /**
+   * 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.
+   */
+  public function processBatchSet();
+
+  /**
+   * Returns the batch set being currently processed.
+   *
+   * @return array
+   *   The current batch set.
+   */
+  public function currentSet();
+
+  /**
+   * 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 array
+   *   TRUE if a subsequent set was found in the batch.
+   */
+  public function nextSet();
+
+  /**
+   * 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
+   *   A redirect response.
+   */
+  public function finished();
+
+  /**
+   * @return array
+   */
+  public function &getBatch();
+
+}
diff --git a/core/lib/Drupal/Core/Batch/BatchQueueHandler.php b/core/lib/Drupal/Core/Batch/BatchQueueHandler.php
new file mode 100644
index 0000000..f3c3137
--- /dev/null
+++ b/core/lib/Drupal/Core/Batch/BatchQueueHandler.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\Batch\BatchQueueHandler
+ */
+
+namespace Drupal\Core\Batch;
+
+use Drupal\Core\Database\Database;
+
+/**
+ * Provides a batch queue handler.
+ */
+class BatchQueueHandler implements BatchQueueHandlerInterface {
+
+  /**
+   * The cache of queues.
+   *
+   * @var \Drupal\Core\Queue\QueueInterface[]
+   */
+  protected $queues;
+
+  /**
+   * Constructs a new BatchQueueHandler.
+   */
+  public function __construct() {
+    $this->queues = array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQueue($batch_set) {
+    if (isset($batch_set['queue'])) {
+      $name = $batch_set['queue']['name'];
+      $class = $batch_set['queue']['class'];
+
+      if (!isset($this->queues[$class][$name])) {
+        $this->queues[$class][$name] = new $class($name, Database::getConnection());
+      }
+      return $this->queues[$class][$name];
+    }
+  }
+
+  /**
+   * Populates a job queue with the operations of a batch set.
+   *
+   * Depending on whether the batch is progressive or not, the
+   * Drupal\Core\Queue\Batch or Drupal\Core\Queue\BatchMemory handler classes
+   * will be used.
+   *
+   * @param array $batch
+   *   The batch array.
+   * @param string $set_id
+   *   The id of the set to process.
+   *
+   * @return array
+   *   The name and class of the queue are added by reference to the batch set.
+   */
+  public function populateQueue(&$batch, $set_id) {
+    $batch_set = &$batch['sets'][$set_id];
+
+    if (isset($batch_set['operations'])) {
+      $batch_set += array(
+        'queue' => array(
+          'name' => 'drupal_batch:' . $batch['id'] . ':' . $set_id,
+          'class' => $batch['progressive'] ? 'Drupal\Core\Queue\Batch' : 'Drupal\Core\Queue\BatchMemory',
+        ),
+      );
+
+      $queue = $this->getQueue($batch_set);
+      $queue->createQueue();
+      foreach ($batch_set['operations'] as $operation) {
+        $queue->createItem($operation);
+      }
+
+      unset($batch_set['operations']);
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Batch/BatchQueueHandlerInterface.php b/core/lib/Drupal/Core/Batch/BatchQueueHandlerInterface.php
new file mode 100644
index 0000000..2334c44
--- /dev/null
+++ b/core/lib/Drupal/Core/Batch/BatchQueueHandlerInterface.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\Batch\BatchQueueHandlerInterface
+ */
+
+namespace Drupal\Core\Batch;
+
+/**
+ * @todo add class comment.
+ */
+interface BatchQueueHandlerInterface {
+
+  /**
+   * Returns a queue object for a batch set.
+   *
+   * @param array $batch_set
+   *   The batch set.
+   *
+   * @return \Drupal\Core\Queue\QueueInterface
+   *   The queue object.
+   */
+  public function getQueue($batch_set);
+
+  /**
+   * Populates a job queue with the operations of a batch set.
+   *
+   * Depending on whether the batch is progressive or not, the
+   * Drupal\Core\Queue\Batch or Drupal\Core\Queue\BatchMemory handler classes
+   * will be used.
+   *
+   * @param array $batch
+   *   The batch array.
+   * @param string $set_id
+   *   The id of the set to process.
+   *
+   * @return array
+   *   The name and class of the queue are added by reference to the batch set.
+   */
+  public function populateQueue(&$batch, $set_id);
+
+}
diff --git a/core/lib/Drupal/Core/Batch/Percentage.php b/core/lib/Drupal/Core/Batch/Percentage.php
index 6cb1bef..9a5d234 100644
--- a/core/lib/Drupal/Core/Batch/Percentage.php
+++ b/core/lib/Drupal/Core/Batch/Percentage.php
@@ -29,7 +29,7 @@ class Percentage {
    *   until we are finished, but we also never print more decimal places than
    *   are meaningful.
    *
-   * @see _batch_process()
+   * @see \Drupal\Core\Batch\BatchProcessorInterface::processBatchSet()
    */
   public static function format($total, $current) {
     if (!$total || $total == $current) {
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index fe6d104..18c01d8 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -206,7 +206,8 @@ public function buildForm($form_id, FormStateInterface &$form_state) {
 
     if (isset($_SESSION['batch_form_state'])) {
       // We've been redirected here after a batch processing. The form has
-      // already been processed, but needs to be rebuilt. See _batch_finished().
+      // already been processed, but needs to be rebuilt. See
+      // \Drupal\Core\Batch\BatchProcessorInterface::finished().
       $form_state = $_SESSION['batch_form_state'];
       unset($_SESSION['batch_form_state']);
       return $this->rebuildForm($form_id, $form_state);
diff --git a/core/lib/Drupal/Core/Form/FormSubmitter.php b/core/lib/Drupal/Core/Form/FormSubmitter.php
index a9bd0ac..9c1599c 100644
--- a/core/lib/Drupal/Core/Form/FormSubmitter.php
+++ b/core/lib/Drupal/Core/Form/FormSubmitter.php
@@ -71,8 +71,9 @@ public function doSubmitForm(&$form, FormStateInterface &$form_state) {
 
       // Execution continues only for programmatic forms.
       // For 'regular' forms, we get redirected to the batch processing
-      // page. Form redirection will be handled in _batch_finished(),
-      // after the batch is processed.
+      // page. Form redirection will be handled in
+      // \Drupal\Core\Batch\BatchProcessorInterface::finished(), after the batch
+      // is processed.
     }
 
     // Set a flag to indicate the form has been processed and executed.
@@ -108,7 +109,7 @@ public function executeSubmitHandlers(&$form, FormStateInterface &$form_state) {
       if (($batch = &$this->batchGet()) && !isset($batch['id'])) {
         // Some previous submit handler has set a batch. To ensure correct
         // execution order, store the call in a special 'control' batch set.
-        // See _batch_next_set().
+        // See \Drupal\Core\Batch\BatchProcessorInterface::nextSet().
         $batch['sets'][] = array('form_submit' => $callback);
         $batch['has_form_submits'] = TRUE;
       }
diff --git a/core/tests/Drupal/Tests/Core/Batch/BatchProcessorTest.php b/core/tests/Drupal/Tests/Core/Batch/BatchProcessorTest.php
new file mode 100644
index 0000000..cd992de
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Batch/BatchProcessorTest.php
@@ -0,0 +1,148 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Batch\BatchProcessorTest.
+ */
+
+namespace Drupal\Tests\Core\Batch;
+
+use Drupal\Core\Batch\BatchProcessor;
+use Drupal\Core\Batch\BatchQueueHandlerInterface;
+use Drupal\Core\Batch\BatchStorageInterface;
+use Drupal\Core\Datetime\DateFormatter;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Form\FormSubmitterInterface;
+use Drupal\Core\GeneratedUrl;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\Core\Theme\ActiveTheme;
+use Drupal\Core\Theme\ThemeManagerInterface;
+use Drupal\Core\Url;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\HttpFoundation\ParameterBag;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\Routing\Route;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Batch\BatchProcessor
+ * @group Batch
+ */
+class BatchProcessorTest extends UnitTestCase {
+
+  /**
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The batch queue.
+   *
+   * @var \Drupal\Core\Batch\BatchQueueHandlerInterface
+   */
+  protected $batchQueue;
+
+  /**
+   * The date formatter.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatter
+   */
+  protected $dateFormatter;
+
+  /**
+   * The form submitter.
+   *
+   * @var \Drupal\Core\Form\FormSubmitterInterface
+   */
+  protected $formSubmitter;
+
+  /**
+   * The batch storage.
+   *
+   * @var \Drupal\Core\Batch\BatchStorageInterface
+   */
+  protected $batchStorage;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->batchQueue = $this->prophesize(BatchQueueHandlerInterface::CLASS);
+    $this->dateFormatter = $this->prophesize(DateFormatter::CLASS);
+    $this->formSubmitter = $this->prophesize(FormSubmitterInterface::CLASS);
+    $this->batchStorage = $this->prophesize(BatchStorageInterface::CLASS);
+    $this->requestStack = new RequestStack();
+  }
+
+  protected function setupBatchProcessor() {
+    return new BatchProcessor('core', $this->batchQueue->reveal(), $this->dateFormatter->reveal(), $this->formSubmitter->reveal(), $this->batchStorage->reveal(), $this->requestStack);
+  }
+
+  protected function setupThemeManager() {
+    $this->themeManager->getActiveTheme()->willReturn(new ActiveTheme(['name' => 'example']));
+  }
+
+  protected function setupRouteMatch() {
+    $this->routeMatch->getRouteObject()->willReturn(new Route('/test'));
+    $this->routeMatch->getRouteName()->willReturn('test');
+    $this->routeMatch->getRawParameters()->willReturn(new ParameterBag());
+  }
+
+  protected function setupRequest() {
+    $request = Request::create('/test');
+    $this->requestStack->push($request);
+  }
+
+  protected function setupBasicBatch() {
+    $batch = [
+      'current_set' => 0,
+      'progressive' => TRUE,
+      'url' => Url::fromRoute('system.batch_page.html'),
+      'source_url' => Url::fromRoute('<front>'),
+      'theme' => 'example_theme',
+      'sets' => [],
+    ];
+    return $batch;
+  }
+
+  protected function setBatchWithOneSet() {
+    $batch = $this->setupBasicBatch();
+    $batch['sets'][0] =
+  }
+
+  /**
+   * @covers ::getBatch
+   */
+  public function testGetEmptyBatch() {
+    $batch = $this->setupBatchProcessor();
+
+    $this->assertEquals(['sets' => []], $batch->getBatch());
+  }
+
+// @todo Throws an assertion ATM.
+//  public function testProcessBatchSetWithNoCurrentSet() {
+//    $batch = $this->setupBatchProcessor();
+//
+//    $batch->processBatchSet();
+//  }
+
+  public function testProcessBatchSetWithInitialSet() {
+    $this->batchStorage->load()->willReturn($this->setupBasicBatch());
+
+    $batch_processor = $this->setupBatchProcessor();
+
+    $batch = &$batch_processor->getBatch();
+    $batch = $this->setupBasicBatch();
+
+    $result = $batch_processor->processBatchSet();
+    print_r($result);
+  }
+
+}
+
