diff --git a/core/core.services.yml b/core/core.services.yml
index 2f04124..480ef39 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -785,6 +785,8 @@ services:
     arguments: ['@database']
     tags:
       - { name: backend_overridable }
+  batch.processor:
+    class: Drupal\Core\Batch\BatchProcessor
   replica_database_ignore__subscriber:
     class: Drupal\Core\EventSubscriber\ReplicaDatabaseIgnoreSubscriber
     tags:
diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 8a8f7f2..99d8bc3 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -210,133 +210,11 @@ function _batch_progress_page() {
  *
  * @return
  *   An array containing a completion value (in percent) and a status message.
+ *
+ * @deprecated Use \Drupal::service('batch.processor')->process() instead.
  */
 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_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 ($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 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) && _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();
-  }
+  return \Drupal::service('batch.processor')->process();
 }
 
 /**
@@ -357,6 +235,8 @@ function _batch_process() {
  *   are meaningful.
  *
  * @see _batch_process()
+ *
+ * @deprecated Use \Drupal\Core\Batch\Percentage::format() instead.
  */
 function _batch_api_percentage($total, $current) {
   return Percentage::format($total, $current);
@@ -364,10 +244,11 @@ function _batch_api_percentage($total, $current) {
 
 /**
  * Returns the batch set being currently processed.
+ *
+ * @deprecated Use \Drupal::service('batch.processor')->currentSet() instead.
  */
 function &_batch_current_set() {
-  $batch = &batch_get();
-  return $batch['sets'][$batch['current_set']];
+  return \Drupal::service('batch.processor')->currentSet();
 }
 
 /**
@@ -379,20 +260,11 @@ function &_batch_current_set() {
  *
  * @return
  *   TRUE if a subsequent set was found in the batch.
+ *
+ * @deprecated Use \Drupal::service('batch.processor')->nextSet() instead.
  */
 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;
-  }
+  return \Drupal::service('batch.processor')->nextSet();
 }
 
 /**
@@ -402,90 +274,7 @@ function _batch_next_set() {
  * the results and resolve page redirection.
  */
 function _batch_finished() {
-  $batch = &batch_get();
-
-  // 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();
-        call_user_func_array($batch_set['finished'], array($batch_set['success'], $batch_set['results'], $operations, \Drupal::service('date.formatter')->formatInterval($batch_set['elapsed'] / 1000)));
-      }
-    }
-  }
-
-  // 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 (!isset($_batch['form_state'])) {
-      $_batch['form_state'] = new FormState();
-    }
-    if ($_batch['form_state']->getRedirect() === NULL) {
-      $redirect = $_batch['batch_redirect'] ?: $_batch['source_url'];
-      $options = UrlHelper::parse($redirect);
-      if (!UrlHelper::isExternal($options['path'])) {
-        $options['path'] = $GLOBALS['base_url'] . '/' . $options['path'];
-      }
-      $redirect = Url::createFromPath($options['path']);
-      $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 (!empty($_batch['form_state']['rebuild'])) {
-      $_SESSION['batch_form_state'] = $_batch['form_state'];
-    }
-    $callback = $_batch['redirect_callback'];
-    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 = url($_batch['source_url'], array(
-        'absolute' => TRUE,
-        'query' => array('op' => 'finish', 'id' => $_batch['id']),
-      ));
-      return new RedirectResponse($url);
-    }
-  }
+  \Drupal::service('batch.processor')->finished();
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Batch/BatchProcessor.php b/core/lib/Drupal/Core/Batch/BatchProcessor.php
new file mode 100644
index 0000000..2e9dfd2
--- /dev/null
+++ b/core/lib/Drupal/Core/Batch/BatchProcessor.php
@@ -0,0 +1,285 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\Batch\BatchProcessor
+ */
+
+namespace Drupal\Core\Batch;
+
+use Drupal\Component\Utility\Timer;
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Url;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * Provides a batch processor.
+ */
+class BatchProcessor implements BatchProcessorInterface {
+
+  /**
+   * The set of batches.
+   *
+   * @var array
+   */
+  protected $batch;
+
+  /**
+   * Creates a new BatchProcessor.
+   */
+  public function __construct() {
+    $this->batch = array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process() {
+    $batch       = &$this->batch;
+    $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 ($batch['progressive']) {
+      Timer::start('batch_processing');
+    }
+
+    if (empty($current_set['start'])) {
+      $current_set['start'] = microtime(TRUE);
+    }
+
+    include_once DRUPAL_ROOT . '/form.inc';
+    /** @var \Drupal\Core\Queue\QueueInterface $queue */
+    $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 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 = _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 = 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'    => \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 $this->finished();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function currentSet() {
+    return $this->batch['sets'][$this->batch['current_set']];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function nextSet() {
+    $batch = &$this->batch;
+    if (isset($batch['sets'][$batch['current_set'] + 1])) {
+      $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 = &$batch['form_state']->getCompleteForm();
+        call_user_func_array($callback, array(&$complete_form, &$batch['form_state']));
+      }
+      return TRUE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function finished() {
+    $batch = &$this->batch;
+
+    // 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'])) {
+          /** @var \Drupal\Core\Queue\QueueInterface $queue */
+          $queue = _batch_queue($batch_set);
+          $operations = $queue->getAllItems();
+          call_user_func_array($batch_set['finished'], array($batch_set['success'], $batch_set['results'], $operations, \Drupal::service('date.formatter')->formatInterval($batch_set['elapsed'] / 1000)));
+        }
+      }
+    }
+
+    // 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 (!isset($_batch['form_state'])) {
+        $_batch['form_state'] = new FormState();
+      }
+      if ($_batch['form_state']->getRedirect() === NULL) {
+        $redirect = $_batch['batch_redirect'] ?: $_batch['source_url'];
+        $options = UrlHelper::parse($redirect);
+        if (!UrlHelper::isExternal($options['path'])) {
+          $options['path'] = $GLOBALS['base_url'] . '/' . $options['path'];
+        }
+        $redirect = Url::createFromPath($options['path']);
+        $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 (!empty($_batch['form_state']['rebuild'])) {
+        $_SESSION['batch_form_state'] = $_batch['form_state'];
+      }
+      $callback = $_batch['redirect_callback'];
+      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 = url($_batch['source_url'], array(
+          'absolute' => TRUE,
+          'query' => array('op' => 'finish', 'id' => $_batch['id']),
+        ));
+        return new RedirectResponse($url);
+      }
+    }
+  }
+
+
+}
diff --git a/core/lib/Drupal/Core/Batch/BatchProcessorInterface.php b/core/lib/Drupal/Core/Batch/BatchProcessorInterface.php
new file mode 100644
index 0000000..d5e3444
--- /dev/null
+++ b/core/lib/Drupal/Core/Batch/BatchProcessorInterface.php
@@ -0,0 +1,56 @@
+<?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 process();
+
+  /**
+   * 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.
+   */
+  public function finished();
+
+}
