diff --git a/core/core.services.yml b/core/core.services.yml
index 6310c21..b60d424 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1639,3 +1639,6 @@ services:
     class: Drupal\Core\EventSubscriber\RssResponseRelativeUrlFilter
     tags:
       - { name: event_subscriber }
+  pager.factory:
+    class: Drupal\Core\Pager\PagerFactory
+    arguments: ['@service_container', '@request_stack']
diff --git a/core/globals.api.php b/core/globals.api.php
index 1487b19..0f846e6 100644
--- a/core/globals.api.php
+++ b/core/globals.api.php
@@ -78,7 +78,14 @@
  *
  * The array index is the pager element index (0 by default).
  *
- * @see pager_default_initialize()
+ * @deprecated as of Drupal 8.3.x, will be removed before Drupal 9.0.0.
+ *   Do not directly set or get values from this array. Use the methods
+ *   provided by \Drupal\Core\Pager\PagerFactoryInterface and
+ *   \Drupal\Core\Pager\PagerInterface instead.
+ *
+ * @see \Drupal\Core\Pager\PagerFactoryInterface::get()
+ * @see \Drupal\Core\Pager\PagerInterface::init()
+ * @see \Drupal\Core\Pager\PagerInterface::getLimit()
  */
 global $pager_limits;
 
@@ -87,7 +94,14 @@
  *
  * The array index is the pager element index (0 by default).
  *
- * @see pager_default_initialize()
+ * @deprecated as of Drupal 8.3.x, will be removed before Drupal 9.0.0.
+ *   Do not directly set or get values from this array. Use the methods
+ *   provided by \Drupal\Core\Pager\PagerFactoryInterface and
+ *   \Drupal\Core\Pager\PagerInterface instead.
+ *
+ * @see \Drupal\Core\Pager\PagerFactoryInterface::get()
+ * @see \Drupal\Core\Pager\PagerInterface::init()
+ * @see \Drupal\Core\Pager\PagerInterface::getCurrentPage()
  */
 global $pager_page_array;
 
@@ -96,7 +110,14 @@
  *
  * The array index is the pager element index (0 by default).
  *
- * @see pager_default_initialize()
+ * @deprecated as of Drupal 8.3.x, will be removed before Drupal 9.0.0.
+ *   Do not directly set or get values from this array. Use the methods
+ *   provided by \Drupal\Core\Pager\PagerFactoryInterface and
+ *   \Drupal\Core\Pager\PagerInterface instead.
+ *
+ * @see \Drupal\Core\Pager\PagerFactoryInterface::get()
+ * @see \Drupal\Core\Pager\PagerInterface::init()
+ * @see \Drupal\Core\Pager\PagerInterface::getTotalPages()
  */
 global $pager_total;
 
@@ -105,6 +126,13 @@
  *
  * The array index is the pager element index (0 by default).
  *
- * @see pager_default_initialize()
+ * @deprecated as of Drupal 8.3.x, will be removed before Drupal 9.0.0.
+ *   Do not directly set or get values from this array. Use the methods
+ *   provided by \Drupal\Core\Pager\PagerFactoryInterface and
+ *   \Drupal\Core\Pager\PagerInterface instead.
+ *
+ * @see \Drupal\Core\Pager\PagerFactoryInterface::get()
+ * @see \Drupal\Core\Pager\PagerInterface::init()
+ * @see \Drupal\Core\Pager\PagerInterface::getTotalItems()
  */
 global $pager_total_items;
diff --git a/core/includes/pager.inc b/core/includes/pager.inc
index 6b6bbb7..f4aae0a 100644
--- a/core/includes/pager.inc
+++ b/core/includes/pager.inc
@@ -5,8 +5,6 @@
  * Functions to aid in presenting database results as a set of pages.
  */
 
-use Drupal\Component\Utility\UrlHelper;
-
 /**
  * Returns the current page being requested for display within a pager.
  *
@@ -23,15 +21,14 @@
  *   even though the default pager implementation adjusts for this and still
  *   displays the third page of search results at that URL.
  *
- * @see pager_default_initialize()
+ * @deprecated as of Drupal 8.3.x, will be removed before Drupal 9.0.0.
+ *   Use \Drupal::service('pager.factory')->get($element)->getCurrentPage()
+ *   instead.
+ *
+ * @see \Drupal\Core\Pager\PagerInterface::init()
  */
 function pager_find_page($element = 0) {
-  $page = \Drupal::request()->query->get('page', '');
-  $page_array = explode(',', $page);
-  if (!isset($page_array[$element])) {
-    $page_array[$element] = 0;
-  }
-  return (int) $page_array[$element];
+  return \Drupal::service('pager.factory')->get($element)->getCurrentPage();
 }
 
 /**
@@ -61,7 +58,7 @@ function pager_find_page($element = 0) {
  *   $where = "status = 1";
  *   $total = mymodule_select("SELECT COUNT(*) FROM data " . $where)->result();
  *   $num_per_page = \Drupal::config('mymodule.settings')->get('num_per_page');
- *   $page = pager_default_initialize($total, $num_per_page);
+ *   $page = \Drupal::service('pager.factory')->get(0)->init($total, $num_per_page)->getCurrentPage();
  *
  *   // Next, retrieve the items for the current page and put them into a
  *   // render array.
@@ -84,17 +81,20 @@ function pager_find_page($element = 0) {
  * this information). Here, we call pager_find_page() to calculate the desired
  * offset before the search is invoked:
  * @code
- *   // Perform the query, using the requested offset from pager_find_page().
- *   // This comes from a URL parameter, so here we are assuming that the URL
- *   // parameter corresponds to an actual page of results that will exist
- *   // within the set.
- *   $page = pager_find_page();
+ *   // First, get a pager for the element '0'.
+ *   $pager = \Drupal::service('pager.factory')->get(0);
+ *
+ *   // Perform the query, using the requested offset from
+ *   // $pager->getCurrentPage(). This comes from a URL parameter, so here we
+ *   // are assuming that the URL parameter corresponds to an actual page of
+ *   // results that will exist within the set.
+ *   $page = $pager->getCurrentPage();
  *   $num_per_page = \Drupal::config('mymodule.settings')->get('num_per_page');
  *   $offset = $num_per_page * $page;
  *   $result = mymodule_remote_search($keywords, $offset, $num_per_page);
  *
  *   // Now that we have the total number of results, initialize the pager.
- *   pager_default_initialize($result->total, $num_per_page);
+ *   $pager->init($result->total, $num_per_page);
  *
  *   // Create a render array with the search results.
  *   $render = [];
@@ -123,18 +123,15 @@ function pager_find_page($element = 0) {
  *   that does not correspond to the actual range of the result set was
  *   requested, this function will return the closest page actually within the
  *   result set.
+ *
+ * @deprecated as of Drupal 8.3.x, will be removed before Drupal 9.0.0.
+ *   Use \Drupal::service('pager.factory')->get($element)->init($total, $limit)
+ *   to initialize the pager, and
+ *   \Drupal::service('pager.factory')->get($element)->getCurrentPage() to
+ *   retrieve the current page.
  */
 function pager_default_initialize($total, $limit, $element = 0) {
-  global $pager_page_array, $pager_total, $pager_total_items, $pager_limits;
-
-  $page = pager_find_page($element);
-
-  // We calculate the total of pages as ceil(items / limit).
-  $pager_total_items[$element] = $total;
-  $pager_total[$element] = ceil($pager_total_items[$element] / $limit);
-  $pager_page_array[$element] = max(0, min($page, ((int) $pager_total[$element]) - 1));
-  $pager_limits[$element] = $limit;
-  return $pager_page_array[$element];
+  return \Drupal::service('pager.factory')->get($element)->init($total, $limit)->getCurrentPage();
 }
 
 /**
@@ -143,13 +140,16 @@ function pager_default_initialize($total, $limit, $element = 0) {
  * @return array
  *   A URL query parameter array that consists of all components of the current
  *   page request except for those pertaining to paging.
+ *
+ * @deprecated as of Drupal 8.3.x, will be removed before Drupal 9.0.0.
+ *   Use
+ *   \Drupal::service('pager.factory')->getCurrentRequestQueryParameters(['page'])
+ *   instead.
  */
 function pager_get_query_parameters() {
-  $query = &drupal_static(__FUNCTION__);
-  if (!isset($query)) {
-    $query = UrlHelper::filterQueryParameters(\Drupal::request()->query->all(), array('page'));
-  }
-  return $query;
+  $pager_factory = \Drupal::service('pager.factory');
+  $pager_parameter = $pager_factory->getPagerParameterName();
+  return $pager_factory->getCurrentRequestQueryParameters([$pager_parameter]);
 }
 
 /**
@@ -173,105 +173,93 @@ function pager_get_query_parameters() {
  *     - #quantity: The number of pages in the list.
  */
 function template_preprocess_pager(&$variables) {
-  $element = $variables['pager']['#element'];
   $parameters = $variables['pager']['#parameters'];
   $quantity = $variables['pager']['#quantity'];
   $route_name = $variables['pager']['#route_name'];
   $route_parameters = isset($variables['pager']['#route_parameters']) ? $variables['pager']['#route_parameters'] : [];
-  global $pager_page_array, $pager_total;
+
+  // Prepare the pager object.
+  $pager = \Drupal::service('pager.factory')->get($variables['pager']['#element']);
 
   // Nothing to do if there is only one page.
-  if ($pager_total[$element] <= 1) {
+  if ($pager->getTotalPages() <= 1) {
     return;
   }
 
   $tags = $variables['pager']['#tags'];
 
-  // Calculate various markers within this pager piece:
+  // Calculate various markers within this pager list:
   // Middle is used to "center" pages around the current page.
-  $pager_middle = ceil($quantity / 2);
-  // current is the page we are currently paged to.
-  $pager_current = $pager_page_array[$element] + 1;
-  // first is the first page listed by this pager piece (re quantity).
-  $pager_first = $pager_current - $pager_middle + 1;
-  // last is the last page listed by this pager piece (re quantity).
-  $pager_last = $pager_current + $quantity - $pager_middle;
-  // max is the maximum page number.
-  $pager_max = $pager_total[$element];
+  $list_middle = ceil($quantity / 2);
+  // Current is the page we are currently paged to.
+  $list_current = $pager->getCurrentPage() + 1;
+  // First is the first page listed by this pager list (re quantity).
+  $list_first = $list_current - $list_middle + 1;
+  // Last is the last page listed by this pager list (re quantity).
+  $list_last = $list_current + $quantity - $list_middle;
   // End of marker calculations.
 
   // Prepare for generation loop.
-  $i = $pager_first;
-  if ($pager_last > $pager_max) {
+  $i = $list_first;
+  if ($list_last > $pager->getTotalPages()) {
     // Adjust "center" if at end of query.
-    $i = $i + ($pager_max - $pager_last);
-    $pager_last = $pager_max;
+    $i = $i + ($pager->getTotalPages() - $list_last);
+    $list_last = $pager->getTotalPages();
   }
   if ($i <= 0) {
     // Adjust "center" if at start of query.
-    $pager_last = $pager_last + (1 - $i);
+    $list_last = $list_last + (1 - $i);
     $i = 1;
   }
   // End of generation loop preparation.
 
   // Create the "first" and "previous" links if we are not on the first page.
-  if ($pager_page_array[$element] > 0) {
-    $items['first'] = array();
-    $options = array(
-      'query' => pager_query_add_page($parameters, $element, 0),
-    );
-    $items['first']['href'] = \Drupal::url($route_name, $route_parameters, $options);
+  if ($pager->getCurrentPage() > 0) {
+    $items['first'] = [
+      'href' => $pager->toUrl(0, $parameters, $route_name, $route_parameters),
+    ];
     if (isset($tags[0])) {
       $items['first']['text'] = $tags[0];
     }
 
-    $items['previous'] = array();
-    $options = array(
-      'query' => pager_query_add_page($parameters, $element, $pager_page_array[$element] - 1),
-    );
-    $items['previous']['href'] = \Drupal::url($route_name, $route_parameters, $options);
+    $items['previous'] = [
+      'href' => $pager->toUrl($pager->getCurrentPage() - 1, $parameters, $route_name, $route_parameters),
+    ];
     if (isset($tags[1])) {
       $items['previous']['text'] = $tags[1];
     }
   }
 
-  if ($i != $pager_max) {
+  if ($i != $pager->getTotalPages()) {
     // Add an ellipsis if there are further previous pages.
     if ($i > 1) {
       $variables['ellipses']['previous'] = TRUE;
     }
     // Now generate the actual pager piece.
-    for (; $i <= $pager_last && $i <= $pager_max; $i++) {
-      $options = array(
-        'query' => pager_query_add_page($parameters, $element, $i - 1),
-      );
-      $items['pages'][$i]['href'] = \Drupal::url($route_name, $route_parameters, $options);
-      if ($i == $pager_current) {
+    for (; $i <= $list_last && $i <= $pager->getTotalPages(); $i++) {
+      $items['pages'][$i]['href'] = $pager->toUrl($i - 1, $parameters, $route_name, $route_parameters);
+      if ($i == $list_current) {
         $variables['current'] = $i;
       }
     }
     // Add an ellipsis if there are further next pages.
-    if ($i < $pager_max + 1) {
+    if ($i < $pager->getTotalPages() + 1) {
       $variables['ellipses']['next'] = TRUE;
     }
   }
 
   // Create the "next" and "last" links if we are not on the last page.
-  if ($pager_page_array[$element] < ($pager_max - 1)) {
-    $items['next'] = array();
-    $options = array(
-      'query' => pager_query_add_page($parameters, $element, $pager_page_array[$element] + 1),
-    );
-    $items['next']['href'] = \Drupal::url($route_name, $route_parameters, $options);
+  if ($pager->getCurrentPage() < ($pager->getTotalPages() - 1)) {
+    $items['next'] = [
+      'href' => $pager->toUrl($pager->getCurrentPage() + 1, $parameters, $route_name, $route_parameters),
+    ];
     if (isset($tags[3])) {
       $items['next']['text'] = $tags[3];
     }
 
-    $items['last'] = array();
-    $options = array(
-      'query' => pager_query_add_page($parameters, $element, $pager_max - 1),
-    );
-    $items['last']['href'] = \Drupal::url($route_name, $route_parameters, $options);
+    $items['last'] = [
+      'href' => $pager->toUrl($pager->getTotalPages() - 1, $parameters, $route_name, $route_parameters),
+    ];
     if (isset($tags[4])) {
       $items['last']['text'] = $tags[4];
     }
@@ -307,25 +295,16 @@ function template_preprocess_pager(&$variables) {
  *
  * @return array
  *   The altered $query parameter array.
+ *
+ * @deprecated as of Drupal 8.3.x, will be removed before Drupal 9.0.0.
+ *   Use
+ *   \Drupal::service('pager.factory')->get($element)->getLinkQueryParameters($index, $query)
+ *   instead. However, normally this function should no longer be called, the
+ *   the PagerInterface object can return a full URL link via
+ *   PagerInterface::toUrl().
+ *
+ * @see \Drupal\Core\Pager\PagerInterface
  */
 function pager_query_add_page(array $query, $element, $index) {
-  global $pager_page_array;
-
-  // Build the 'page' query parameter. This is built based on the current
-  // page of each pager element (or NULL if the pager is not set), with the
-  // exception of the requested page index for the current element.
-  $max_element = max(array_keys($pager_page_array));
-  $element_pages = [];
-  for ($i = 0; $i <= $max_element; $i++) {
-    $element_pages[] = ($i == $element) ? $index : (isset($pager_page_array[$i]) ? $pager_page_array[$i] : NULL);
-  }
-  $query['page'] = implode(',', $element_pages);
-
-  // Merge the query parameters passed to this function with the parameters
-  // from the current request. In case of collision, the parameters passed into
-  // this function take precedence.
-  if ($current_request_query = pager_get_query_parameters()) {
-    $query = array_merge($current_request_query, $query);
-  }
-  return $query;
+  return \Drupal::service('pager.factory')->get($element)->getLinkQueryParameters($index, $query);
 }
diff --git a/core/lib/Drupal/Core/Pager/Pager.php b/core/lib/Drupal/Core/Pager/Pager.php
new file mode 100644
index 0000000..96d5ed6
--- /dev/null
+++ b/core/lib/Drupal/Core/Pager/Pager.php
@@ -0,0 +1,249 @@
+<?php
+
+namespace Drupal\Core\Pager;
+
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Pager class.
+ */
+class Pager implements PagerInterface {
+
+  /**
+   * The pager element.
+   *
+   * This is the index used by query extenders to identify the query
+   * to be paged, and reflected in the 'page=x,y,z' query parameter
+   * of the HTTP request.
+   *
+   * @var int
+   */
+  protected $element;
+
+  /**
+   * Number of items per page.
+   *
+   * @var int
+   */
+  protected $limit;
+
+  /**
+   * Current page number, 0-indexed.
+   *
+   * @var int
+   */
+  protected $currentPage;
+
+  /**
+   * Total number of pages.
+   *
+   * @var int
+   */
+  protected $totalPages;
+
+  /**
+   * Total number of items.
+   *
+   * @var int
+   */
+  protected $totalItems;
+
+  /**
+   * The pager factory.
+   *
+   * @var \Drupal\Core\Pager\PagerFactoryInterface
+   */
+  protected $factory;
+
+  /**
+   * Constructs a new Pager object.
+   *
+   * @param \Drupal\Core\Pager\PagerFactoryInterface $pager_factory
+   *   The pager factory.
+   */
+  public function __construct(PagerFactoryInterface $pager_factory) {
+    $this->factory = $pager_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $element) {
+    $instance = new static(
+      $container->get('pager.factory')
+    );
+
+    // Set the pager element.
+    $instance->element = $element;
+
+    // @todo: Start of BC layer. This maps the pager properties to the global
+    // pager variables of D8. When removing the global variables in D9, remove
+    // the following lines.
+    global $pager_page_array, $pager_total, $pager_total_items, $pager_limits;
+    if (!isset($pager_page_array[$element])) {
+      $pager_page_array[$element] = NULL;
+    }
+    $instance->currentPage = &$pager_page_array[$element];
+    if (!isset($pager_total[$element])) {
+      $pager_total[$element] = NULL;
+    }
+    $instance->totalPages = &$pager_total[$element];
+    if (!isset($pager_total_items[$element])) {
+      $pager_total_items[$element] = NULL;
+    }
+    $instance->totalItems = &$pager_total_items[$element];
+    if (!isset($pager_limits[$element])) {
+      $pager_limits[$element] = NULL;
+    }
+    $instance->limit = &$pager_limits[$element];
+    // @todo: End of BC layer.
+
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getElement() {
+    return $this->element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function init($total, $limit) {
+    // The total number of pages to be managed by the pager is given by the
+    // total number of items ($total) divided by the number of items in each
+    // page ($limit). We take the nearest integer to accomodate any remainder.
+    $this
+      ->setTotalItems($total)
+      ->setTotalPages((int) ceil($this->getTotalItems() / $limit))
+      ->setCurrentPage(max(0, min($this->getCurrentPage(), ((int) $this->getTotalPages() - 1))))
+      ->setLimit($limit);
+    return $this;
+  }
+
+  /**
+   * Gets the current page (0-index), from the request query parameters.
+   *
+   * @return int
+   *   The page to which the pager is currently positioned to.
+   */
+  protected function getCurrentPageFromRequest() {
+    $page_query = $this->factory->getCurrentRequestQueryParameter($this->factory->getPagerParameterName());
+    $page_array = explode(',', $page_query);
+    if (!isset($page_array[$this->element])) {
+      $page_array[$this->element] = 0;
+    }
+    return (int) $page_array[$this->element];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCurrentPage() {
+    if (!isset($this->currentPage) || $this->currentPage === NULL) {
+      $this->setCurrentPage($this->getCurrentPageFromRequest());
+    }
+    return $this->currentPage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCurrentPage($page) {
+    $this->currentPage = (int) $page;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTotalPages() {
+    return $this->totalPages;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTotalPages($total_pages) {
+    $this->totalPages = (int) $total_pages;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLastPage() {
+    return max(0, $this->getTotalPages() - 1);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTotalItems() {
+    return $this->totalItems;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTotalItems($total_items) {
+    $this->totalItems = (int) $total_items;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLimit() {
+    return $this->limit;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLimit($limit) {
+    $this->limit = (int) $limit;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLinkQueryParameters($page, array $query_parameters) {
+    // Get all defined pagers.
+    $pagers = $this->factory->all();
+
+    // Build the pager query parameter. This is built based on the current
+    // page of each pager element (or NULL if the pager is not set), with the
+    // exception of the requested page index for the current element.
+    $query = $query_parameters;
+    $max_element = max(array_keys($pagers));
+    $element_pages = [];
+    for ($i = 0; $i <= $max_element; $i++) {
+      $element_pages[] = ($i == $this->getElement()) ? $page : (isset($pagers[$i]) ? $pagers[$i]->getCurrentPage() : NULL);
+    }
+    $parameter = $this->factory->getPagerParameterName();
+    $query[$parameter] = implode(',', $element_pages);
+
+    // Merge the query parameters passed to this function with the parameters
+    // from the current request. In case of collision, the parameters passed into
+    // this function take precedence.
+    if ($current_request_query = $this->factory->getCurrentRequestQueryParameters()) {
+      $query = array_merge($current_request_query, $query);
+    }
+
+    return $query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function toUrl($page, array $query_parameters, $route_name, array $route_parameters) {
+    $options = ['query' => $this->getLinkQueryParameters($page, $query_parameters)];
+    return Url::fromRoute($route_name, $route_parameters, $options);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Pager/PagerFactory.php b/core/lib/Drupal/Core/Pager/PagerFactory.php
new file mode 100644
index 0000000..1dd8114
--- /dev/null
+++ b/core/lib/Drupal/Core/Pager/PagerFactory.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace Drupal\Core\Pager;
+
+use Drupal\Component\Utility\UrlHelper;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Provides a factory for pagers.
+ */
+class PagerFactory implements PagerFactoryInterface {
+
+  /**
+   * The array of pager objects, indexed by their element.
+   *
+   * @var \Drupal\Core\Pager\PagerInterface[]
+   */
+  protected $pagers = [];
+
+  /**
+   * The service container.
+   *
+   * @var \Symfony\Component\DependencyInjection\ContainerInterface
+   */
+  protected $container;
+
+  /**
+   * The request stack object.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * Constructs a new PagerFactory object.
+   *
+   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+   *   The service container.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   */
+  public function __construct(ContainerInterface $container, RequestStack $request_stack) {
+    $this->container = $container;
+    $this->requestStack = $request_stack;
+  }
+
+  /**
+   * Creates a new PagerInterface object.
+   *
+   * @param int $element
+   *   The pager element.
+   *
+   * @return \Drupal\Core\Pager\PagerInterface
+   *   The pager object.
+   */
+  protected function create($element) {
+    return Pager::create($this->container, $element);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get($element) {
+    if (!isset($this->pagers[$element])) {
+      $this->pagers[$element] = $this->create($element);
+    }
+    return $this->pagers[$element];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFirstAvailableElement() {
+    // @todo: BC layer. When removing the globals, return element from
+    // max(array_keys($this->pagers)) directly.
+    global $pager_total;
+    return (!empty($pager_total) && is_array($pager_total)) ? max(array_keys($pager_total)) + 1 : 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function all() {
+    // @todo: BC layer. When removing the globals, remove the following and
+    // return $this->pagers directly.
+    global $pager_total;
+    if (!empty($pager_total) && is_array($pager_total)) {
+      foreach (array_keys($pager_total) as $element) {
+        $this->get($element);
+      }
+    }
+    return $this->pagers;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPagerParameterName() {
+    return 'page';
+  }
+
+  /**
+   * Returns the current request query parameter bag.
+   *
+   * @return \Symfony\Component\HttpFoundation\ParameterBag
+   *   The request query parameter bag.
+   */
+  protected function getCurrentRequestQuery() {
+    return $this->requestStack->getCurrentRequest()->query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCurrentRequestQueryParameter($parameter) {
+    return $this->getCurrentRequestQuery()->get($parameter, '');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCurrentRequestQueryParameters(array $filter = []) {
+    // @todo: BC layer. In Drupal 9.0.0, remove the $filter argument and just
+    // return the current request query.
+    if (!$filter) {
+      return $this->getCurrentRequestQuery()->all();
+    }
+    static $query, $filtered;
+    if (!isset($query) || $filter !== $filtered) {
+      $parameter = $this->getPagerParameterName();
+      $query = UrlHelper::filterQueryParameters($this->getCurrentRequestQuery()->all(), [$parameter]);
+      $filtered = $filter;
+    }
+    return $query;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Pager/PagerFactoryInterface.php b/core/lib/Drupal/Core/Pager/PagerFactoryInterface.php
new file mode 100644
index 0000000..0f1e17b
--- /dev/null
+++ b/core/lib/Drupal/Core/Pager/PagerFactoryInterface.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\Core\Pager;
+
+/**
+ * Provides an interface for pager factory objects.
+ */
+interface PagerFactoryInterface {
+
+  /**
+   * Returns the pager object for the specified pager element.
+   *
+   * The pager gets created if not already existing.
+   *
+   * @param int $element
+   *   The pager element.
+   *
+   * @return \Drupal\Core\Pager\Pager
+   *   The pager object.
+   */
+  public function get($element);
+
+  /**
+   * Returns the first available pager element to use.
+   *
+   * @return int
+   *   A pager element.
+   */
+  public function getFirstAvailableElement();
+
+  /**
+   * Returns the array of pager objects.
+   *
+   * All the PagerInterface objects instantiated, keyed by pager element.
+   *
+   * @return \Drupal\Core\Pager\Pager[]
+   *   The array of pager objects, indexed by their element.
+   */
+  public function all();
+
+  /**
+   * Returns the string to be used in URL querystring to identify the pager.
+   *
+   * Typically, 'page' so that the URL query will look like this:
+   * '?page=x,y,z'.
+   *
+   * @return string
+   *   The string to be used in URL querystring to identify the pager.
+   */
+  public function getPagerParameterName();
+
+  /**
+   * Returns a query string parameter from the current request.
+   *
+   * @param string $parameter
+   *   The query string parameter to return, e.g. 'page'.
+   *
+   * @return string[]
+   *   The requested query parameter.
+   */
+  public function getCurrentRequestQueryParameter($parameter);
+
+  /**
+   * Returns the current request query string, with optional filtering.
+   *
+   * @param array $filter
+   *   An array of query string parameters to remove from the returned query
+   *   parameters, e.g. ['page']. Defaults to empty array.
+   *
+   * @todo: BC layer. In Drupal 9.0.0, remove the $filter argument.
+   *
+   * @return array
+   *   A URL query parameter array that consists of all components of the current
+   *   page request except for those filtered out.
+   */
+  public function getCurrentRequestQueryParameters(array $filter = []);
+
+}
diff --git a/core/lib/Drupal/Core/Pager/PagerInterface.php b/core/lib/Drupal/Core/Pager/PagerInterface.php
new file mode 100644
index 0000000..ecde698
--- /dev/null
+++ b/core/lib/Drupal/Core/Pager/PagerInterface.php
@@ -0,0 +1,176 @@
+<?php
+
+namespace Drupal\Core\Pager;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides an interface for pager objects.
+ */
+interface PagerInterface {
+
+  /**
+   * Instantiates a new instance of this class.
+   *
+   * This is a factory method that returns a new instance of this class. The
+   * factory should pass any needed dependencies into the constructor of this
+   * class, but not the container itself. Every call to this method must return
+   * a new instance of this class; that is, it may not implement a singleton.
+   *
+   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+   *   The service container this instance should use.
+   * @param int $element
+   *   The pager element that uniquely identifies this pager object.
+   */
+  public static function create(ContainerInterface $container, $element);
+
+  /**
+   * Gets the pager element.
+   *
+   * The pager element is the index that uniquely identifies the relevant pager
+   * when there are multiple pagers managed in the same page request. The
+   * 'page' URL query parameter is a comma-delimited string, where each value
+   * is the target content page for the corresponding pager element. For
+   * instance, if we have 5 pagers on a single page, and we want to have a link
+   * to a page that should display the 6th content page for the 3rd pager, and
+   * the 1st content page for all the other pagers, then the URL query will look
+   * like this: ?page=0,0,5,0,0 (page numbering starts at zero).
+   *
+   * @return int
+   *   The pager element.
+   */
+  public function getElement();
+
+  /**
+   * Initializes the pager.
+   *
+   * @param int $total
+   *   The total number of items to be paged.
+   * @param int $limit
+   *   The number of items the calling code will display per page.
+   *
+   * @return \Drupal\Core\Pager\PagerInterface
+   *   The Pager object.
+   */
+  public function init($total, $limit);
+
+  /**
+   * Gets the current page (0-index).
+   *
+   * @return int
+   *   The page to which the pager is currently positioned to.
+   */
+  public function getCurrentPage();
+
+  /**
+   * Sets the current page for this pager (0-index).
+   *
+   * @param int $page
+   *   The page to which the pager is currently positioned to.
+   *
+   * @return \Drupal\Core\Pager\PagerInterface
+   *   The Pager object.
+   */
+  public function setCurrentPage($page);
+
+  /**
+   * Gets total pages in the pager.
+   *
+   * @return int
+   *   The total number of pages managed by the pager.
+   */
+  public function getTotalPages();
+
+  /**
+   * Sets total pages in the pager.
+   *
+   * @param int $total_pages
+   *   The total number of pages managed by the pager.
+   *
+   * @return \Drupal\Core\Pager\PagerInterface
+   *   The Pager object.
+   */
+  public function setTotalPages($total_pages);
+
+  /**
+   * Gets last page in the pager (0-index).
+   *
+   * @return int
+   *   The index of the last page in the pager.
+   */
+  public function getLastPage();
+
+  /**
+   * Gets total items in the pager.
+   *
+   * @return int
+   *   The total number of items (records) managed by the pager.
+   */
+  public function getTotalItems();
+
+  /**
+   * Sets total items in the pager.
+   *
+   * @param int $total_items
+   *   The total number of items (records) managed by the pager.
+   *
+   * @return \Drupal\Core\Pager\PagerInterface
+   *   The Pager object.
+   */
+  public function setTotalItems($total_items);
+
+  /**
+   * Gets the items per page.
+   *
+   * @return int
+   *   The number of items (records) in each page.
+   */
+  public function getLimit();
+
+  /**
+   * Sets the items per page.
+   *
+   * @param int $limit
+   *   The number of items (records) in each page.
+   *
+   * @return \Drupal\Core\Pager\PagerInterface
+   *   The Pager object.
+   */
+  public function setLimit($limit);
+
+  /**
+   * Gets the query parameters for a pager URL.
+   *
+   * @param int $page
+   *   The target page.
+   * @param string[] $query_parameters
+   *   An array of query parameters to override existing ones.
+   *
+   * @return string[]
+   *   The query parameters array.
+   *
+   * @todo change to protected when pager_query_add_page() is removed maybe?
+   */
+  public function getLinkQueryParameters($page, array $query_parameters);
+
+  /**
+   * Gets the pager URL for a specific page.
+   *
+   * Returns an URL object that can be used in theme preprocessing to render
+   * the link to a target page.
+   *
+   * @param int $page
+   *   The target page.
+   * @param string[] $query_parameters
+   *   An array of query parameters to override existing ones.
+   * @param string $route_name
+   *   The route name.
+   * @param string[] $route_parameters
+   *   The route parameters.
+   *
+   * @return \Drupal\Core\Url
+   *   The URL of the pager link.
+   */
+  public function toUrl($page, array $query_parameters, $route_name, array $route_parameters);
+
+}
diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc
index 94c519e..4f5ee62 100644
--- a/core/modules/views/views.theme.inc
+++ b/core/modules/views/views.theme.inc
@@ -1012,31 +1012,26 @@ function template_preprocess_views_exposed_form(&$variables) {
  *     exposed input.
  */
 function template_preprocess_views_mini_pager(&$variables) {
-  global $pager_page_array, $pager_total;
-
   $tags = &$variables['tags'];
   $element = $variables['element'];
   $parameters = $variables['parameters'];
 
+  // Prepare the pager object.
+  $pager = \Drupal::service('pager.factory')->get($element);
+
   // Current is the page we are currently paged to.
-  $variables['items']['current'] = $pager_page_array[$element] + 1;
+  $variables['items']['current'] = $pager->getCurrentPage() + 1;
 
-  if ($pager_total[$element] > 1 && $pager_page_array[$element] > 0) {
-    $options = array(
-      'query' => pager_query_add_page($parameters, $element, $pager_page_array[$element] - 1),
-    );
-    $variables['items']['previous']['href'] = \Drupal::url('<current>', [], $options);
+  if ($pager->getTotalPages() > 1 && $pager->getCurrentPage() > 0) {
+    $variables['items']['previous']['href'] = $pager->toUrl($pager->getCurrentPage() - 1, $parameters, '<current>', []);
     if (isset($tags[1])) {
       $variables['items']['previous']['text'] = $tags[1];
     }
     $variables['items']['previous']['attributes'] = new Attribute();
   }
 
-  if ($pager_page_array[$element] < ($pager_total[$element] - 1)) {
-    $options = array(
-      'query' => pager_query_add_page($parameters, $element, $pager_page_array[$element] + 1),
-    );
-    $variables['items']['next']['href'] = \Drupal::url('<current>', [], $options);
+  if ($pager->getCurrentPage() < ($pager->getTotalPages() - 1)) {
+    $variables['items']['next']['href'] = $pager->toUrl($pager->getCurrentPage() + 1, $parameters, '<current>', []);
     if (isset($tags[3])) {
       $variables['items']['next']['text'] = $tags[3];
     }
diff --git a/core/tests/Drupal/Tests/Core/Pager/PagerFactoryTest.php b/core/tests/Drupal/Tests/Core/Pager/PagerFactoryTest.php
new file mode 100644
index 0000000..ad06fd9
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Pager/PagerFactoryTest.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Drupal\Tests\Core\Pager;
+
+use Drupal\Tests\UnitTestCase;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Pager\PagerFactory;
+use Drupal\Core\Pager\Pager;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\ParameterBag;
+
+/**
+ * Tests the PagerFactory class.
+ *
+ * @coversDefaultClass \Drupal\Core\Pager\PagerFactory
+ * @group Pager
+ */
+class PagerFactoryTest extends UnitTestCase {
+
+  /**
+   * PagerFactory object.
+   *
+   * @var \Drupal\Core\Pager\PagerFactory
+   */
+  protected $pagerFactory;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $container = new ContainerBuilder();
+    $request_stack = $this->getMockBuilder(RequestStack::class)->getMock();
+    $query = new ParameterBag(['foo' => 'bar', 'qux' => 'dee']);
+
+    $this->pagerFactory = $this
+      ->getMockBuilder(PagerFactory::class)
+      ->setMethods(['getCurrentRequestQuery'])
+      ->setConstructorArgs([$container, $request_stack])
+      ->getMock();
+    $container->set('pager.factory', $this->pagerFactory);
+
+    $this->pagerFactory->expects($this->any())
+      ->method('getCurrentRequestQuery')
+      ->will($this->returnValue($query));
+  }
+
+  /**
+   * @backupGlobals disabled
+   * @covers ::get
+   */
+  public function testGet() {
+    // @todo remove the backupGlobals directive once $pager_page_array,
+    //   $pager_total, $pager_total_items, $pager_limits globals have been
+    //   removed.
+    // Get pager 0.
+    $pager = $this->pagerFactory->get(0);
+    // Pager element should be 0.
+    $this->assertSame(0, $pager->getElement());
+    // Get pager 2.
+    $pager = $this->pagerFactory->get(2);
+    // Pager element should be 2.
+    $this->assertSame(2, $pager->getElement());
+  }
+
+  /**
+   * @backupGlobals disabled
+   * @covers ::getFirstAvailableElement
+   */
+  public function testGetFirstAvailableElement() {
+    // @todo remove the backupGlobals directive once $pager_page_array,
+    //   $pager_total, $pager_total_items, $pager_limits globals have been
+    //   removed.
+    // No pagers set yet, first available element should be 0.
+    $this->assertSame(0, $this->pagerFactory->getFirstAvailableElement());
+    // Get pager 0.
+    $pager = $this->pagerFactory->get(0);
+    // Now first available element should be 1.
+    $this->assertSame(1, $this->pagerFactory->getFirstAvailableElement());
+    // Get pager 4.
+    $pager = $this->pagerFactory->get(4);
+    // Now first available element should be 5.
+    $this->assertSame(5, $this->pagerFactory->getFirstAvailableElement());
+  }
+
+  /**
+   * @backupGlobals disabled
+   * @covers ::all
+   */
+  public function testAll() {
+    // @todo remove the backupGlobals directive once $pager_page_array,
+    //   $pager_total, $pager_total_items, $pager_limits globals have been
+    //   removed.
+    // Get pager 4.
+    $pager = $this->pagerFactory->get(4);
+    $this->assertSame(4, $pager->getElement());
+    // Get pager 6.
+    $pager = $this->pagerFactory->get(6);
+    $this->assertSame(6, $pager->getElement());
+    // We should get two pagers for elements 4 and 6.
+    $this->assertSame(2, count($this->pagerFactory->all()));
+    $this->assertSame([4, 6], array_keys($this->pagerFactory->all()));
+  }
+
+  /**
+   * @covers ::getPagerParameterName
+   */
+  public function testGetPagerParameterName() {
+    $this->assertSame('page', $this->pagerFactory->getPagerParameterName());
+  }
+
+  /**
+   * @covers ::getCurrentRequestQueryParameters
+   */
+  public function testGetCurrentRequestQueryParameters() {
+    $this->assertSame(['foo' => 'bar', 'qux' => 'dee'], $this->pagerFactory->getCurrentRequestQueryParameters());
+  }
+
+  /**
+   * @covers ::getCurrentRequestQueryParameter
+   */
+  public function testGetCurrentRequestQueryParameter() {
+    $this->assertSame('bar', $this->pagerFactory->getCurrentRequestQueryParameter('foo'));
+    $this->assertSame('dee', $this->pagerFactory->getCurrentRequestQueryParameter('qux'));
+    // Missing parameter returns empty string.
+    $this->assertSame('', $this->pagerFactory->getCurrentRequestQueryParameter('zot'));
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Pager/PagerTest.php b/core/tests/Drupal/Tests/Core/Pager/PagerTest.php
new file mode 100644
index 0000000..88673f7
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Pager/PagerTest.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Drupal\Tests\Core\Pager;
+
+use Drupal\Tests\UnitTestCase;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Pager\PagerFactory;
+use Drupal\Core\Pager\Pager;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\ParameterBag;
+
+/**
+ * Tests the Pager class.
+ *
+ * @todo remove the backupGlobals directive once $pager_page_array,
+ *   $pager_total, $pager_total_items, $pager_limits globals have been removed.
+ *
+ * @backupGlobals disabled
+ * @coversDefaultClass \Drupal\Core\Pager\Pager
+ * @group Pager
+ */
+class PagerTest extends UnitTestCase {
+
+  /**
+   * Pager object.
+   *
+   * @var \Drupal\Core\Pager\Pager
+   */
+  protected $pager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $container = new ContainerBuilder();
+    $pager_factory = $this
+      ->getMockBuilder(PagerFactory::class)
+      ->disableOriginalConstructor()
+      ->getMock();
+    $container->set('pager.factory', $pager_factory);
+
+    // Set pager query string parameter to 'page'.
+    $pager_factory->expects($this->any())
+      ->method('getPagerParameterName')
+      ->will($this->returnValue('page'));
+    // Set page query parameter current page to 11 (0-indexed).
+    $pager_factory->expects($this->any())
+      ->method('getCurrentRequestQueryParameter')
+      ->will($this->returnValue(',,,,,,11'));
+    // Set other dummy query parameters.
+    $pager_factory->expects($this->any())
+      ->method('getCurrentRequestQueryParameters')
+      ->will($this->returnValue(['rec' => 'lop', 'ste' => 'cik']));
+
+    // Pager for element 6.
+    $this->pager = Pager::create($container, 6);
+
+    // Set factory to return the test pager via ::all().
+    $pager_factory->expects($this->any())
+      ->method('all')
+      ->will($this->returnValue([6 => $this->pager]));
+
+  }
+
+  /**
+   * @covers ::getElement
+   */
+  public function testGetElement() {
+    $this->assertSame(6, $this->pager->getElement());
+  }
+
+  /**
+   * @covers ::init
+   */
+  public function testInit() {
+    $this->pager->init(513, 28);
+    $this->assertSame(513, $this->pager->getTotalItems());
+    $this->assertSame(19, $this->pager->getTotalPages());
+    $this->assertSame(11, $this->pager->getCurrentPage());
+    $this->assertSame(28, $this->pager->getLimit());
+  }
+
+  /**
+   * @covers ::setCurrentPage
+   * @covers ::getCurrentPage
+   */
+  public function testCurrentPage() {
+    $this->assertSame(11, $this->pager->getCurrentPage());
+    $this->pager->setCurrentPage(200);
+    $this->assertSame(200, $this->pager->getCurrentPage());
+  }
+
+  /**
+   * @covers ::setTotalPages
+   * @covers ::getTotalPages
+   * @covers ::getLastPage
+   */
+  public function testTotalPages() {
+    $this->pager->setTotalPages(40);
+    $this->assertSame(40, $this->pager->getTotalPages());
+    $this->assertSame(39, $this->pager->getLastPage());
+  }
+
+  /**
+   * @covers ::setTotalItems
+   * @covers ::getTotalItems
+   */
+  public function testTotalItems() {
+    $this->pager->setTotalItems(600);
+    $this->assertSame(600, $this->pager->getTotalItems());
+  }
+
+  /**
+   * @covers ::setLimit
+   * @covers ::getLimit
+   */
+  public function testLimit() {
+    $this->pager->setLimit(45);
+    $this->assertSame(45, $this->pager->getLimit());
+  }
+
+  /**
+   * @covers ::getLinkQueryParameters
+   */
+  public function testGetLinkQueryParameters() {
+    $this->assertSame(['rec' => 'lop', 'ste' => 'dee', 'page' => ',,,,,,33'], $this->pager->getLinkQueryParameters(33, ['ste' => 'dee']));
+  }
+
+}
