diff --git a/core/modules/book/book.module b/core/modules/book/book.module
index fca17ab8ab5b301467f3861553fa2eeaa559f0cb..efc718bdd09d3596902486a14ccbd59dcfceb556 100644
--- a/core/modules/book/book.module
+++ b/core/modules/book/book.module
@@ -147,21 +147,6 @@ function book_node_links_alter(array &$node_links, NodeInterface $node, array &$
   }
 }
 
-/**
- * Returns an array of all books.
- *
- * @todo Remove in favor of BookManager Service. http://drupal.org/node/1963894
- *
- * This list may be used for generating a list of all the books, or for building
- * the options for a form select.
- *
- * @return
- *   An array of all books.
- */
-function book_get_books() {
-  return \Drupal::service('book.manager')->getAllBooks();
-}
-
 /**
  * Implements hook_form_BASE_FORM_ID_alter() for node_form().
  *
@@ -244,164 +229,14 @@ function book_form_update($form, $form_state) {
   return $form['book']['pid'];
 }
 
-/**
- * Gets the book menu tree for a page and returns it as a linear array.
- *
- * @param $book_link
- *   A fully loaded menu link that is part of the book hierarchy.
- *
- * @return
- *   A linear array of menu links in the order that the links are shown in the
- *   menu, so the previous and next pages are the elements before and after the
- *   element corresponding to the current node. The children of the current node
- *   (if any) will come immediately after it in the array, and links will only
- *   be fetched as deep as one level deeper than $book_link.
- */
-function book_get_flat_menu($book_link) {
-  $flat = &drupal_static(__FUNCTION__, array());
-
-  if (!isset($flat[$book_link['nid']])) {
-    // Call bookTreeAllData() to take advantage of caching.
-    $tree = \Drupal::service('book.manager')->bookTreeAllData($book_link['bid'], $book_link, $book_link['depth'] + 1);
-    $flat[$book_link['nid']] = array();
-    _book_flatten_menu($tree, $flat[$book_link['nid']]);
-  }
-
-  return $flat[$book_link['nid']];
-}
-
-/**
- * Recursively converts a tree of menu links to a flat array.
- *
- * @param $tree
- *   A tree of menu links in an array.
- * @param $flat
- *   A flat array of the menu links from $tree, passed by reference.
- *
- * @see book_get_flat_menu().
- */
-function _book_flatten_menu($tree, &$flat) {
-  foreach ($tree as $data) {
-    $flat[$data['link']['nid']] = $data['link'];
-    if ($data['below']) {
-      _book_flatten_menu($data['below'], $flat);
-    }
-  }
-}
-
-/**
- * Fetches the menu link for the previous page of the book.
- *
- * @param $book_link
- *   A fully loaded menu link that is part of the book hierarchy.
- *
- * @return
- *   A fully loaded menu link for the page before the one represented in
- *   $book_link.
- */
-function book_prev($book_link) {
-  // If the parent is zero, we are at the start of a book.
-  if ($book_link['pid'] == 0) {
-    return NULL;
-  }
-  // Assigning the array to $flat resets the array pointer for use with each().
-  $flat = book_get_flat_menu($book_link);
-  $curr = NULL;
-  do {
-    $prev = $curr;
-    list($key, $curr) = each($flat);
-  } while ($key && $key != $book_link['nid']);
-
-  if ($key == $book_link['nid']) {
-    /** @var \Drupal\book\BookManagerInterface $book_manager */
-    $book_manager = \Drupal::service('book.manager');
-    // The previous page in the book may be a child of the previous visible link.
-    if ($prev['depth'] == $book_link['depth']) {
-      // The subtree will have only one link at the top level - get its data.
-      $tree = $book_manager->bookMenuSubtreeData($prev);
-      $data = array_shift($tree);
-      // The link of interest is the last child - iterate to find the deepest one.
-      while ($data['below']) {
-        $data = end($data['below']);
-      }
-      $book_manager->bookLinkTranslate($data['link']);
-      return $data['link'];
-    }
-    else {
-      $book_manager->bookLinkTranslate($prev);
-      return $prev;
-    }
-  }
-}
-
-/**
- * Fetches the menu link for the next page of the book.
- *
- * @param $book_link
- *   A fully loaded menu link that is part of the book hierarchy.
- *
- * @return
- *   A fully loaded book link for the page after the one represented in
- *   $book_link.
- */
-function book_next($book_link) {
-  // Assigning the array to $flat resets the array pointer for use with each().
-  $flat = book_get_flat_menu($book_link);
-  do {
-    list($key, ) = each($flat);
-  }
-  while ($key && $key != $book_link['nid']);
-
-  if ($key == $book_link['nid']) {
-    $next = current($flat);
-    if ($next) {
-      \Drupal::service('book.manager')->bookLinkTranslate($next);
-    }
-    return $next;
-  }
-}
-
-/**
- * Formats the menu links for the child pages of the current page.
- *
- * @param $book_link
- *   A fully loaded menu link that is part of the book hierarchy.
- *
- * @return
- *   HTML for the links to the child pages of the current page.
- */
-function book_children($book_link) {
-  $flat = book_get_flat_menu($book_link);
-
-  $children = array();
-
-  if ($book_link['has_children']) {
-    // Walk through the array until we find the current page.
-    do {
-      $link = array_shift($flat);
-    }
-    while ($link && ($link['nid'] != $book_link['nid']));
-    // Continue though the array and collect the links whose parent is this page.
-    while (($link = array_shift($flat)) && $link['pid'] == $book_link['nid']) {
-      $data['link'] = $link;
-      $data['below'] = '';
-      $children[] = $data;
-    }
-  }
-
-  if ($children) {
-    $elements = \Drupal::service('book.manager')->bookTreeOutput($children);
-    return drupal_render($elements);
-  }
-  return '';
-}
-
 /**
  * Implements hook_node_load().
  */
 function book_node_load($nodes) {
-  $result = db_query("SELECT * FROM {book} WHERE nid IN (:nids)", array(':nids' =>  array_keys($nodes)), array('fetch' => PDO::FETCH_ASSOC));
-  foreach ($result as $record) {
+  /** @var \Drupal\book\BookManagerInterface $book_manager */
+  $book_manager = \Drupal::service('book.manager');
+  $links = $book_manager->loadBookLinks(array_keys($nodes), FALSE);
+  foreach ($links as $record) {
     $nodes[$record['nid']]->book = $record;
     $nodes[$record['nid']]->book['link_path'] = 'node/' . $record['nid'];
     $nodes[$record['nid']]->book['link_title'] = $nodes[$record['nid']]->label();
@@ -442,28 +277,18 @@ function book_node_presave(EntityInterface $node) {
  * Implements hook_node_insert().
  */
 function book_node_insert(EntityInterface $node) {
+  /** @var \Drupal\book\BookManagerInterface $book_manager */
   $book_manager = \Drupal::service('book.manager');
-  if (!empty($node->book['bid'])) {
-    if ($node->book['bid'] == 'new') {
-      // New nodes that are their own book.
-      $node->book['bid'] = $node->id();
-    }
-    $book_manager->updateOutline($node);
-  }
+  $book_manager->updateOutline($node);
 }
 
 /**
  * Implements hook_node_update().
  */
 function book_node_update(EntityInterface $node) {
+  /** @var \Drupal\book\BookManagerInterface $book_manager */
   $book_manager = \Drupal::service('book.manager');
-  if (!empty($node->book['bid'])) {
-    if ($node->book['bid'] == 'new') {
-      // New nodes that are their own book.
-      $node->book['bid'] = $node->id();
-    }
-    $book_manager->updateOutline($node);
-  }
+  $book_manager->updateOutline($node);
 }
 
 /**
@@ -481,7 +306,7 @@ function book_node_predelete(EntityInterface $node) {
  * Implements hook_node_prepare_form().
  */
 function book_node_prepare_form(NodeInterface $node, $operation, array &$form_state) {
-  // Get BookManager service
+  /** @var \Drupal\book\BookManagerInterface $book_manager */
   $book_manager = \Drupal::service('book.manager');
 
   // Prepare defaults for the add/edit form.
@@ -584,12 +409,15 @@ function template_preprocess_book_navigation(&$variables) {
   $variables['current_depth'] = $book_link['depth'];
   $variables['tree'] = '';
 
+  /** @var \Drupal\book\BookOutline $book_outline */
+  $book_outline = \Drupal::service('book.outline');
+
   if ($book_link['nid']) {
-    $variables['tree'] = book_children($book_link);
+    $variables['tree'] = $book_outline->childrenLinks($book_link);
 
     $build = array();
 
-    if ($prev = book_prev($book_link)) {
+    if ($prev = $book_outline->prevLink($book_link)) {
       $prev_href = \Drupal::url('node.view', array('node' => $prev['nid']));
       $build['#attached']['drupal_add_html_head_link'][][] = array(
         'rel' => 'prev',
@@ -611,7 +439,7 @@ function template_preprocess_book_navigation(&$variables) {
       $variables['parent_title'] = String::checkPlain($parent['title']);
     }
 
-    if ($next = book_next($book_link)) {
+    if ($next = $book_outline->nextLink($book_link)) {
       $next_href = \Drupal::url('node.view', array('node' => $next['nid']));
       $build['#attached']['drupal_add_html_head_link'][][] = array(
         'rel' => 'next',
diff --git a/core/modules/book/book.services.yml b/core/modules/book/book.services.yml
index daa995a814fea79dc381e823d0710752164053d7..8dc0ec47f9ff9d3ad3e8213977346b9e453567d5 100644
--- a/core/modules/book/book.services.yml
+++ b/core/modules/book/book.services.yml
@@ -7,9 +7,12 @@ services:
   book.manager:
     class: Drupal\book\BookManager
     arguments: ['@database', '@entity.manager', '@string_translation', '@config.factory']
+  book.outline:
+    class: Drupal\book\BookOutline
+    arguments: ['@book.manager']
   book.export:
     class: Drupal\book\BookExport
-    arguments: ['@entity.manager']
+    arguments: ['@entity.manager', '@book.manager']
 
   access_check.book.removable:
     class: Drupal\book\Access\BookNodeIsRemovableAccessCheck
diff --git a/core/modules/book/lib/Drupal/book/BookExport.php b/core/modules/book/lib/Drupal/book/BookExport.php
index b03de15e5efea6426573fdee4f4a3ece528d7e04..f28f855dd8e111b2bad48b4d58942e048bf1629a 100644
--- a/core/modules/book/lib/Drupal/book/BookExport.php
+++ b/core/modules/book/lib/Drupal/book/BookExport.php
@@ -31,15 +31,25 @@ class BookExport {
    */
   protected $viewBuilder;
 
+  /**
+   * The book manager.
+   *
+   * @var \Drupal\book\BookManagerInterface
+   */
+  protected $bookManager;
+
   /**
    * Constructs a BookExport object.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entityManager
    *   The entity manager.
+   * @param \Drupal\book\BookManagerInterface $book_manager
+   *   The book manager.
    */
-  public function __construct(EntityManagerInterface $entityManager) {
+  public function __construct(EntityManagerInterface $entityManager, BookManagerInterface $book_manager) {
     $this->nodeStorage = $entityManager->getStorage('node');
     $this->viewBuilder = $entityManager->getViewBuilder('node');
+    $this->bookManager = $book_manager;
   }
 
   /**
@@ -67,7 +77,7 @@ public function bookExportHtml(NodeInterface $node) {
       throw new \Exception();
     }
 
-    $tree = \Drupal::service('book.manager')->bookMenuSubtreeData($node->book);
+    $tree = $this->bookManager->bookSubtreeData($node->book);
     $contents = $this->exportTraverse($tree, array($this, 'bookNodeExport'));
     return array(
       '#theme' => 'book_export_html',
diff --git a/core/modules/book/lib/Drupal/book/BookManager.php b/core/modules/book/lib/Drupal/book/BookManager.php
index e17f9e72d3f120143940a29784d21227d2e33556..04a16550ca5264fa3db16de690efff7f55403cdc 100644
--- a/core/modules/book/lib/Drupal/book/BookManager.php
+++ b/core/modules/book/lib/Drupal/book/BookManager.php
@@ -61,6 +61,13 @@ class BookManager implements BookManagerInterface {
    */
   protected $books;
 
+  /**
+   * Stores flattened book trees.
+   *
+   * @var array
+   */
+  protected $bookTreeFlattened;
+
   /**
    * Constructs a BookManager object.
    */
@@ -135,25 +142,32 @@ public function getParentDepthLimit(array $book_link) {
   }
 
   /**
-   * {@inheritdoc}
+   * Determine the relative depth of the children of a given book link.
+   *
+   * @param array
+   *   The book link.
+   *
+   * @return int
+   *   The difference between the max depth in the book tree and the depth of
+   *   the passed book link.
    */
-  protected function findChildrenRelativeDepth(array $entity) {
-    $query = db_select('book');
+  protected function findChildrenRelativeDepth(array $book_link) {
+    $query = $this->connection->select('book');
     $query->addField('book', 'depth');
-    $query->condition('bid', $entity['bid']);
+    $query->condition('bid', $book_link['bid']);
     $query->orderBy('depth', 'DESC');
     $query->range(0, 1);
 
     $i = 1;
     $p = 'p1';
-    while ($i <= static::BOOK_MAX_DEPTH && $entity[$p]) {
-      $query->condition($p, $entity[$p]);
+    while ($i <= static::BOOK_MAX_DEPTH && $book_link[$p]) {
+      $query->condition($p, $book_link[$p]);
       $p = 'p' . ++$i;
     }
 
     $max_depth = $query->execute()->fetchField();
 
-    return ($max_depth > $entity['depth']) ? $max_depth - $entity['depth'] : 0;
+    return ($max_depth > $book_link['depth']) ? $max_depth - $book_link['depth'] : 0;
   }
 
   /**
@@ -253,6 +267,12 @@ public function updateOutline(NodeInterface $node) {
     if (empty($node->book['bid'])) {
       return FALSE;
     }
+
+    if (!empty($node->book['bid']) && $node->book['bid'] == 'new') {
+      // New nodes that are their own book.
+      $node->book['bid'] = $node->id();
+    }
+
     // Ensure we create a new book link if either the node itself is new, or the
     // bid was selected the first time, so that the original_bid is still empty.
     $new = empty($node->book['nid']) || empty($node->book['original_bid']);
@@ -320,7 +340,7 @@ protected function t($string, array $args = array(), array $options = array()) {
    * existing form element.
    *
    * @param array $book_link
-   *   A fully loaded menu link that is part of the book hierarchy.
+   *   A fully loaded book link that is part of the book hierarchy.
    *
    * @return array
    *   A parent selection form element.
@@ -366,24 +386,24 @@ protected function addParentSelectFormElements(array $book_link) {
   }
 
   /**
-   * Recursively processes and formats menu items for getTableOfContents().
+   * Recursively processes and formats book links for getTableOfContents().
    *
    * This helper function recursively modifies the table of contents array for
-   * each item in the menu tree, ignoring items in the exclude array or at a depth
+   * each item in the book tree, ignoring items in the exclude array or at a depth
    * greater than the limit. Truncates titles over thirty characters and appends
    * an indentation string incremented by depth.
    *
    * @param array $tree
-   *   The data structure of the book's menu tree. Includes hidden links.
+   *   The data structure of the book's outline tree. Includes hidden links.
    * @param string $indent
-   *   A string appended to each menu item title. Increments by '--' per depth
+   *   A string appended to each node title. Increments by '--' per depth
    *   level.
    * @param array $toc
    *   Reference to the table of contents array. This is modified in place, so the
    *   function does not have a return value.
    * @param array $exclude
-   *   Optional array of menu link ID values. Any link whose menu link ID is in
-   *   this array will be excluded (along with its children).
+   *   Optional array of Node ID values. Any link whose node ID is in this
+   *   array will be excluded (along with its children).
    * @param int $depth_limit
    *   Any link deeper than this value will be excluded (along with its children).
    */
@@ -482,7 +502,7 @@ public function bookTreeAllData($bid, $link = NULL, $max_depth = NULL) {
       }
 
       // Build the tree using the parameters; the resulting tree will be cached.
-      $tree[$cid] = $this->menu_build_tree($bid, $tree_parameters);
+      $tree[$cid] = $this->bookTreeBuild($bid, $tree_parameters);
     }
 
     return $tree[$cid];
@@ -495,7 +515,7 @@ public function bookTreeOutput(array $tree) {
     $build = array();
     $items = array();
 
-    // Pull out just the menu links we are going to render so that we
+    // Pull out just the book links we are going to render so that we
     // get an accurate count for the first/last classes.
     foreach ($tree as $data) {
       if ($data['link']['access']) {
@@ -514,7 +534,7 @@ public function bookTreeOutput(array $tree) {
       }
       // Set a class for the <li>-tag. Since $data['below'] may contain local
       // tasks, only set 'expanded' class if the link also has children within
-      // the current menu.
+      // the current book.
       if ($data['link']['has_children'] && $data['below']) {
         $class[] = 'expanded';
       }
@@ -530,11 +550,11 @@ public function bookTreeOutput(array $tree) {
         $data['link']['localized_options']['attributes']['class'][] = 'active-trail';
       }
 
-      // Allow menu-specific theme overrides.
+      // Allow book-specific theme overrides.
       $element['#theme'] = 'book_link__book_toc_' . $data['link']['bid'];
       $element['#attributes']['class'] = $class;
       $element['#title'] = $data['link']['title'];
-      $node = \Drupal::entityManager()->getStorage('node')->load($data['link']['nid']);
+      $node = $this->entityManager->getStorage('node')->load($data['link']['nid']);
       $element['#href'] = $node->url();
       $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array();
       $element['#below'] = $data['below'] ? $this->bookTreeOutput($data['below']) : $data['below'];
@@ -554,47 +574,46 @@ public function bookTreeOutput(array $tree) {
   }
 
   /**
-   * Builds a menu tree, translates links, and checks access.
+   * Builds a book tree, translates links, and checks access.
    *
    * @param int $bid
    *   The Book ID to find links for.
    * @param array $parameters
    *   (optional) An associative array of build parameters. Possible keys:
-   *   - expanded: An array of parent link ids to return only menu links that are
-   *     children of one of the plids in this list. If empty, the whole menu tree
+   *   - expanded: An array of parent link ids to return only book links that are
+   *     children of one of the plids in this list. If empty, the whole outline
    *     is built, unless 'only_active_trail' is TRUE.
-   *   - active_trail: An array of mlids, representing the coordinates of the
-   *     currently active menu link.
+   *   - active_trail: An array of nids, representing the coordinates of the
+   *     currently active book link.
    *   - only_active_trail: Whether to only return links that are in the active
    *     trail. This option is ignored, if 'expanded' is non-empty.
-   *   - min_depth: The minimum depth of menu links in the resulting tree.
-   *     Defaults to 1, which is the default to build a whole tree for a menu
-   *     (excluding menu container itself).
-   *   - max_depth: The maximum depth of menu links in the resulting tree.
+   *   - min_depth: The minimum depth of book links in the resulting tree.
+   *     Defaults to 1, which is the default to build a whole tree for a book.
+   *   - max_depth: The maximum depth of book links in the resulting tree.
    *   - conditions: An associative array of custom database select query
    *     condition key/value pairs; see _menu_build_tree() for the actual query.
    *
    * @return array
-   *   A fully built menu tree.
+   *   A fully built book tree.
    */
-  protected function menu_build_tree($bid, array $parameters = array()) {
-    // Build the menu tree.
-    $data = $this->_menu_build_tree($bid, $parameters);
+  protected function bookTreeBuild($bid, array $parameters = array()) {
+    // Build the book tree.
+    $data = $this->doBookTreeBuild($bid, $parameters);
     // Check access for the current user to each item in the tree.
     $this->bookTreeCheckAccess($data['tree'], $data['node_links']);
     return $data['tree'];
   }
 
   /**
-   * Builds a menu tree.
+   * Builds a book tree.
    *
    * This function may be used build the data for a menu tree only, for example
    * to further massage the data manually before further processing happens.
-   * menu_tree_check_access() needs to be invoked afterwards.
+   * _menu_tree_check_access() needs to be invoked afterwards.
    *
    * @see menu_build_tree()
    */
-  protected function _menu_build_tree($bid, array $parameters = array()) {
+  protected function doBookTreeBuild($bid, array $parameters = array()) {
     // Static cache of already built menu trees.
     $trees = &drupal_static(__METHOD__, array());
     $language_interface = \Drupal::languageManager()->getCurrentLanguage();
@@ -609,7 +628,7 @@ protected function _menu_build_tree($bid, array $parameters = array()) {
     // If we do not have this tree in the static cache, check {cache_data}.
     if (!isset($trees[$tree_cid])) {
       $cache = \Drupal::cache('data')->get($tree_cid);
-      if ($cache && isset($cache->data)) {
+      if ($cache && $cache->data) {
         $trees[$tree_cid] = $cache->data;
       }
     }
@@ -646,7 +665,7 @@ protected function _menu_build_tree($bid, array $parameters = array()) {
         $links[$link['nid']] = $link;
       }
       $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array());
-      $data['tree'] = $this->menu_tree_data($links, $active_trail, $min_depth);
+      $data['tree'] = $this->buildBookOutlineData($links, $active_trail, $min_depth);
       $data['node_links'] = array();
       $this->bookTreeCollectNodeLinks($data['tree'], $data['node_links']);
 
@@ -665,26 +684,70 @@ public function bookTreeCollectNodeLinks(&$tree, &$node_links) {
     // All book links are nodes.
     // @todo clean this up.
     foreach ($tree as $key => $v) {
-      if ($v['link']['nid']) {
-        $nid = $v['link']['nid'];
-        $node_links[$nid][$tree[$key]['link']['nid']] = &$tree[$key]['link'];
-        $tree[$key]['link']['access'] = FALSE;
-      }
+      $nid = $v['link']['nid'];
+      $node_links[$nid][$tree[$key]['link']['nid']] = &$tree[$key]['link'];
+      $tree[$key]['link']['access'] = FALSE;
       if ($tree[$key]['below']) {
         $this->bookTreeCollectNodeLinks($tree[$key]['below'], $node_links);
       }
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function bookTreeGetFlat(array $book_link) {
+    if (!isset($this->bookTreeFlattened[$book_link['nid']])) {
+      // Call $this->bookTreeAllData() to take advantage of caching.
+      $tree = $this->bookTreeAllData($book_link['bid'], $book_link, $book_link['depth'] + 1);
+      $this->bookTreeFlattened[$book_link['nid']] = array();
+      $this->flatBookTree($tree, $this->bookTreeFlattened[$book_link['nid']]);
+    }
+
+    return $this->bookTreeFlattened[$book_link['nid']];
+  }
+
+  /**
+   * Recursively converts a tree of menu links to a flat array.
+   *
+   * @param array $tree
+   *   A tree of menu links in an array.
+   * @param array $flat
+   *   A flat array of the menu links from $tree, passed by reference.
+   *
+   * @see static::bookTreeGetFlat().
+   */
+  protected function flatBookTree(array $tree, array &$flat) {
+    foreach ($tree as $data) {
+      $flat[$data['link']['nid']] = $data['link'];
+      if ($data['below']) {
+        $this->flatBookTree($data['below'], $flat);
+      }
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
   public function loadBookLink($nid, $translate = TRUE) {
-    $link = $this->connection->query("SELECT * FROM {book} WHERE nid = :nid", array(':nid' => $nid))->fetchAssoc();
-    if ($link && $translate) {
-      $this->bookLinkTranslate($link);
+    $links = $this->loadBookLinks(array($nid), $translate);
+    return isset($links[$nid]) ? $links[$nid] : FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadBookLinks($nids, $translate = TRUE) {
+    $result = $this->connection->query("SELECT * FROM {book} WHERE nid IN (:nids)", array(':nids' =>  $nids), array('fetch' => \PDO::FETCH_ASSOC));
+    $links = array();
+    foreach ($result as $link) {
+      if ($translate) {
+        $this->bookLinkTranslate($link);
+      }
+      $links[$link['nid']] = $link;
     }
-    return $link;
+
+    return $links;
   }
 
   /**
@@ -887,8 +950,9 @@ protected function setParents(array &$link, array $parent) {
    */
   public function bookTreeCheckAccess(&$tree, $node_links = array()) {
     if ($node_links) {
+      // @todo Extract that into its own method.
       $nids = array_keys($node_links);
-      $select = db_select('node_field_data', 'n');
+      $select = $this->connection->select('node_field_data', 'n');
       $select->addField('n', 'nid');
       // @todo This should be actually filtering on the desired node status field
       //   language and just fall back to the default language.
@@ -903,20 +967,20 @@ public function bookTreeCheckAccess(&$tree, $node_links = array()) {
         }
       }
     }
-    $this->_menu_tree_check_access($tree);
+    $this->doBookTreeCheckAccess($tree);
   }
 
   /**
    * Sorts the menu tree and recursively checks access for each item.
    */
-  protected function _menu_tree_check_access(&$tree) {
+  protected function doBookTreeCheckAccess(&$tree) {
     $new_tree = array();
     foreach ($tree as $key => $v) {
       $item = &$tree[$key]['link'];
       $this->bookLinkTranslate($item);
       if ($item['access']) {
         if ($tree[$key]['below']) {
-          $this->_menu_tree_check_access($tree[$key]['below']);
+          $this->doBookTreeCheckAccess($tree[$key]['below']);
         }
         // The weights are made a uniform 5 digits by adding 50000 as an offset.
         // After calling $this->bookLinkTranslate(), $item['title'] has the
@@ -955,44 +1019,41 @@ public function bookLinkTranslate(&$link) {
   }
 
   /**
-   * Sorts and returns the built data representing a menu tree.
+   * Sorts and returns the built data representing a book tree.
    *
    * @param array $links
-   *   A flat array of menu links that are part of the menu. Each array element
-   *   is an associative array of information about the menu link, containing the
-   *   fields from the {menu_links} table, and optionally additional information
-   *   from the {menu_router} table, if the menu item appears in both tables.
-   *   This array must be ordered depth-first. See _menu_build_tree() for a sample
-   *   query.
+   *   A flat array of book links that are part of the book. Each array element
+   *   is an associative array of information about the book link, containing the
+   *   fields from the {book} table. This array must be ordered depth-first.
    * @param array $parents
-   *   An array of the menu link ID values that are in the path from the current
-   *   page to the root of the menu tree.
+   *   An array of the node ID values that are in the path from the current
+   *   page to the root of the book tree.
    * @param int $depth
-   *   The minimum depth to include in the returned menu tree.
+   *   The minimum depth to include in the returned book tree.
    *
    * @return array
-   *   An array of menu links in the form of a tree. Each item in the tree is an
+   *   An array of book links in the form of a tree. Each item in the tree is an
    *   associative array containing:
-   *   - link: The menu link item from $links, with additional element
+   *   - link: The book link item from $links, with additional element
    *     'in_active_trail' (TRUE if the link ID was in $parents).
    *   - below: An array containing the sub-tree of this item, where each element
    *     is a tree item array with 'link' and 'below' elements. This array will be
-   *     empty if the menu item has no items in its sub-tree having a depth
+   *     empty if the book link has no items in its sub-tree having a depth
    *     greater than or equal to $depth.
    */
-  protected function menu_tree_data(array $links, array $parents = array(), $depth = 1) {
+  protected function buildBookOutlineData(array $links, array $parents = array(), $depth = 1) {
     // Reverse the array so we can use the more efficient array_pop() function.
     $links = array_reverse($links);
-    return $this->_menu_tree_data($links, $parents, $depth);
+    return $this->buildBookOutlineRecursive($links, $parents, $depth);
   }
 
   /**
-   * Builds the data representing a menu tree.
+   * Builds the data representing a book tree.
    *
    * The function is a bit complex because the rendering of a link depends on
-   * the next menu link.
+   * the next book link.
    */
-  protected function _menu_tree_data(&$links, $parents, $depth) {
+  protected function buildBookOutlineRecursive(&$links, $parents, $depth) {
     $tree = array();
     while ($item = array_pop($links)) {
       // We need to determine if we're on the path to root so we can later build
@@ -1008,8 +1069,8 @@ protected function _menu_tree_data(&$links, $parents, $depth) {
       $next = end($links);
       // Check whether the next link is the first in a new sub-tree.
       if ($next && $next['depth'] > $depth) {
-        // Recursively call _menu_tree_data to build the sub-tree.
-        $tree[$item['nid']]['below'] = $this->_menu_tree_data($links, $parents, $next['depth']);
+        // Recursively call buildBookOutlineRecursive to build the sub-tree.
+        $tree[$item['nid']]['below'] = $this->buildBookOutlineRecursive($links, $parents, $next['depth']);
         // Fetch next link after filling the sub-tree.
         $next = end($links);
       }
@@ -1024,19 +1085,19 @@ protected function _menu_tree_data(&$links, $parents, $depth) {
   /**
    * {@inheritdoc}
    */
-  public function bookMenuSubtreeData($link) {
+  public function bookSubtreeData($link) {
     $tree = &drupal_static(__METHOD__, array());
 
     // Generate a cache ID (cid) specific for this $link.
     $cid = 'book-links:subtree-cid:' . $link['nid'];
 
     if (!isset($tree[$cid])) {
-      $cache = \Drupal::cache('data')->get($cid);
+      $tree_cid_cache = \Drupal::cache('data')->get($cid);
 
-      if ($cache && isset($cache->data)) {
+      if ($tree_cid_cache && $tree_cid_cache->data) {
         // If the cache entry exists, it will just be the cid for the actual data.
         // This avoids duplication of large amounts of data.
-        $cache = \Drupal::cache('data')->get($cache->data);
+        $cache = \Drupal::cache('data')->get($tree_cid_cache->data);
 
         if ($cache && isset($cache->data)) {
           $data = $cache->data;
@@ -1058,7 +1119,7 @@ public function bookMenuSubtreeData($link) {
         foreach ($query->execute() as $item) {
           $links[] = $item;
         }
-        $data['tree'] = $this->menu_tree_data($links, array(), $link['depth']);
+        $data['tree'] = $this->buildBookOutlineData($links, array(), $link['depth']);
         $data['node_links'] = array();
         $this->bookTreeCollectNodeLinks($data['tree'], $data['node_links']);
         // Compute the real cid for book subtree data.
@@ -1068,7 +1129,7 @@ public function bookMenuSubtreeData($link) {
         if (!\Drupal::cache('data')->get($tree_cid)) {
           \Drupal::cache('data')->set($tree_cid, $data, Cache::PERMANENT, array('bid' => $link['bid']));
         }
-        // Cache the cid of the (shared) data using the menu and item-specific cid.
+        // Cache the cid of the (shared) data using the book and item-specific cid.
         \Drupal::cache('data')->set($cid, $tree_cid, Cache::PERMANENT, array('bid' => $link['bid']));
       }
       // Check access for the current user to each item in the tree.
diff --git a/core/modules/book/lib/Drupal/book/BookManagerInterface.php b/core/modules/book/lib/Drupal/book/BookManagerInterface.php
index 18725a98c583eed1f0e1eba7c27483663fe16fe7..c096e68df3f7e2621d29841edb08836c82e72e33 100644
--- a/core/modules/book/lib/Drupal/book/BookManagerInterface.php
+++ b/core/modules/book/lib/Drupal/book/BookManagerInterface.php
@@ -53,6 +53,20 @@ public function bookTreeAllData($bid, $link = NULL, $max_depth = NULL);
    */
   public function loadBookLink($nid, $translate = TRUE);
 
+  /**
+   * Loads multiple book entries.
+   *
+   * @param int[] $nids
+   *   An array of nids to load.
+   *
+   * @param bool $translate
+   *   If TRUE, set access, title, and other elements.
+   *
+   * @return array[]
+   *   The book data of each node keyed by NID.
+   */
+  public function loadBookLinks($nids, $translate = TRUE);
+
   /**
    * Returns an array of book pages in table of contents order.
    *
@@ -94,10 +108,7 @@ public function getParentDepthLimit(array $book_link);
   public function bookTreeCollectNodeLinks(&$tree, &$node_links);
 
   /**
-   * Provides menu link access control, translation, and argument handling.
-   *
-   * This function is similar to _menu_translate(), but it also does
-   * link-specific preparation (such as always calling to_arg() functions).
+   * Provides book loading, access control and translation.
    *
    * @param array $link
    *   A book link.
@@ -107,6 +118,21 @@ public function bookTreeCollectNodeLinks(&$tree, &$node_links);
    */
   public function bookLinkTranslate(&$link);
 
+  /**
+   * Gets the book for a page and returns it as a linear array.
+   *
+   * @param array $book_link
+   *   A fully loaded book link that is part of the book hierarchy.
+   *
+   * @return array
+   *   A linear array of book links in the order that the links are shown in the
+   *   book, so the previous and next pages are the elements before and after the
+   *   element corresponding to the current node. The children of the current node
+   *   (if any) will come immediately after it in the array, and links will only
+   *   be fetched as deep as one level deeper than $book_link.
+   */
+  public function bookTreeGetFlat(array $book_link);
+
   /**
    * Returns an array of all books.
    *
@@ -195,7 +221,7 @@ public function deleteFromBook($nid);
    * - leaf: The menu item has no submenu.
    *
    * @param array $tree
-   *   A data structure representing the tree as returned from menu_tree_data.
+   *   A data structure representing the tree as returned from buildBookOutlineData.
    *
    * @return array
    *   A structured array to be rendered by drupal_render().
@@ -224,12 +250,12 @@ public function bookTreeCheckAccess(&$tree, $node_links = array());
    * tree.
    *
    * @param array $link
-   *   A fully loaded menu link.
+   *   A fully loaded book link.
    *
    * @return
-   *   A subtree of menu links in an array, in the order they should be rendered.
+   *   A subtree of book links in an array, in the order they should be rendered.
    */
-  public function bookMenuSubtreeData($link);
+  public function bookSubtreeData($link);
 
   /**
    * Determines if a node can be removed from the book.
diff --git a/core/modules/book/lib/Drupal/book/BookOutline.php b/core/modules/book/lib/Drupal/book/BookOutline.php
new file mode 100644
index 0000000000000000000000000000000000000000..484fd2c29be8483f65573b74d1bf47c88ee86e1f
--- /dev/null
+++ b/core/modules/book/lib/Drupal/book/BookOutline.php
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\book\BookOutline.
+ */
+
+namespace Drupal\book;
+
+/**
+ * Provides handling to render the book outline.
+ */
+class BookOutline {
+
+  /**
+   * The book manager.
+   *
+   * @var \Drupal\book\BookManagerInterface
+   */
+  protected $bookManager;
+
+  /**
+   * Constructs a new BookOutline.
+   *
+   * @param \Drupal\book\BookManagerInterface $book_manager
+   *   The book manager.
+   */
+  public function __construct(BookManagerInterface $book_manager) {
+    $this->bookManager = $book_manager;
+  }
+
+  /**
+   * Fetches the book link for the previous page of the book.
+   *
+   * @param array $book_link
+   *   A fully loaded book link that is part of the book hierarchy.
+   *
+   * @return array
+   *   A fully loaded book link for the page before the one represented in
+   *   $book_link.
+   */
+  public function prevLink(array $book_link) {
+    // If the parent is zero, we are at the start of a book.
+    if ($book_link['pid'] == 0) {
+      return NULL;
+    }
+    // Assigning the array to $flat resets the array pointer for use with each().
+    $flat = $this->bookManager->bookTreeGetFlat($book_link);
+    $curr = NULL;
+    do {
+      $prev = $curr;
+      list($key, $curr) = each($flat);
+    } while ($key && $key != $book_link['nid']);
+
+    if ($key == $book_link['nid']) {
+      // The previous page in the book may be a child of the previous visible link.
+      if ($prev['depth'] == $book_link['depth']) {
+        // The subtree will have only one link at the top level - get its data.
+        $tree = $this->bookManager->bookSubtreeData($prev);
+        $data = array_shift($tree);
+        // The link of interest is the last child - iterate to find the deepest one.
+        while ($data['below']) {
+          $data = end($data['below']);
+        }
+        $this->bookManager->bookLinkTranslate($data['link']);
+        return $data['link'];
+      }
+      else {
+        $this->bookManager->bookLinkTranslate($prev);
+        return $prev;
+      }
+    }
+  }
+
+  /**
+   * Fetches the book link for the next page of the book.
+   *
+   * @param array $book_link
+   *   A fully loaded book link that is part of the book hierarchy.
+   *
+   * @return array
+   *   A fully loaded book link for the page after the one represented in
+   *   $book_link.
+   */
+  public function nextLink(array $book_link) {
+    // Assigning the array to $flat resets the array pointer for use with each().
+    $flat = $this->bookManager->bookTreeGetFlat($book_link);
+    do {
+      list($key,) = each($flat);
+    } while ($key && $key != $book_link['nid']);
+
+    if ($key == $book_link['nid']) {
+      $next = current($flat);
+      if ($next) {
+        $this->bookManager->bookLinkTranslate($next);
+      }
+      return $next;
+    }
+  }
+
+  /**
+   * Formats the book links for the child pages of the current page.
+   *
+   * @param array $book_link
+   *   A fully loaded book link that is part of the book hierarchy.
+   *
+   * @return array
+   *   HTML for the links to the child pages of the current page.
+   */
+  public function childrenLinks(array $book_link) {
+    $flat = $this->bookManager->bookTreeGetFlat($book_link);
+
+    $children = array();
+
+    if ($book_link['has_children']) {
+      // Walk through the array until we find the current page.
+      do {
+        $link = array_shift($flat);
+      } while ($link && ($link['nid'] != $book_link['nid']));
+      // Continue though the array and collect the links whose parent is this page.
+      while (($link = array_shift($flat)) && $link['pid'] == $book_link['nid']) {
+        $data['link'] = $link;
+        $data['below'] = '';
+        $children[] = $data;
+      }
+    }
+
+    if ($children) {
+      $elements = $this->bookManager->bookTreeOutput($children);
+      return drupal_render($elements);
+    }
+    return '';
+  }
+
+}
diff --git a/core/modules/book/lib/Drupal/book/Form/BookAdminEditForm.php b/core/modules/book/lib/Drupal/book/Form/BookAdminEditForm.php
index e44a2c12a14be4bc22b95b6c32276513297e1357..ef9ed99dcdaed418614bac5b958d7348aae9a093 100644
--- a/core/modules/book/lib/Drupal/book/Form/BookAdminEditForm.php
+++ b/core/modules/book/lib/Drupal/book/Form/BookAdminEditForm.php
@@ -142,7 +142,7 @@ protected function bookAdminTable(NodeInterface $node, array &$form) {
       '#tree' => TRUE,
     );
 
-    $tree = \Drupal::service('book.manager')->bookMenuSubtreeData($node->book);
+    $tree = $this->bookManager->bookSubtreeData($node->book);
     // Do not include the book item itself.
     $tree = array_shift($tree);
     if ($tree['below']) {
diff --git a/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php b/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php
index 6a9cfffd014d98c930e27d888d0b4c0e185b9db5..d98be3bccd89c2dfc2d6d4b84ed0c2e4e7fc9cd8 100644
--- a/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php
+++ b/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php
@@ -8,6 +8,7 @@
 namespace Drupal\book\Plugin\Block;
 
 use Drupal\block\BlockBase;
+use Drupal\book\BookManagerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -30,6 +31,13 @@ class BookNavigationBlock extends BlockBase implements ContainerFactoryPluginInt
    */
   protected $request;
 
+  /**
+   * The book manager.
+   *
+   * @var \Drupal\book\BookManagerInterface
+   */
+  protected $bookManager;
+
   /**
    * Constructs a new BookNavigationBlock instance.
    *
@@ -41,11 +49,14 @@ class BookNavigationBlock extends BlockBase implements ContainerFactoryPluginInt
    *   The plugin implementation definition.
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   The request object.
+   * @param \Drupal\book\BookManagerInterface $book_manager
+   *   The book manager.
    */
-  public function __construct(array $configuration, $plugin_id, array $plugin_definition, Request $request) {
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, Request $request, BookManagerInterface $book_manager) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
 
     $this->request = $request;
+    $this->bookManager = $book_manager;
   }
 
   /**
@@ -56,7 +67,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $configuration,
       $plugin_id,
       $plugin_definition,
-      $container->get('request')
+      $container->get('request'),
+      $container->get('book.manager')
     );
   }
 
@@ -107,12 +119,12 @@ public function build() {
     if ($this->configuration['block_mode'] == 'all pages') {
       $book_menus = array();
       $pseudo_tree = array(0 => array('below' => FALSE));
-      foreach (book_get_books() as $book_id => $book) {
+      foreach ($this->bookManager->getAllBooks() as $book_id => $book) {
         if ($book['bid'] == $current_bid) {
           // If the current page is a node associated with a book, the menu
           // needs to be retrieved.
-          $data = \Drupal::service('book.manager')->bookTreeAllData($node->book['bid'], $node->book);
-          $book_menus[$book_id] = \Drupal::service('book.manager')->bookTreeOutput($data);
+          $data = $this->bookManager->bookTreeAllData($node->book['bid'], $node->book);
+          $book_menus[$book_id] = $this->bookManager->bookTreeOutput($data);
         }
         else {
           // Since we know we will only display a link to the top node, there
@@ -122,7 +134,7 @@ public function build() {
           $book_node = node_load($book['nid']);
           $book['access'] = $book_node->access('view');
           $pseudo_tree[0]['link'] = $book;
-          $book_menus[$book_id] = \Drupal::service('book.manager')->bookTreeOutput($pseudo_tree);
+          $book_menus[$book_id] = $this->bookManager->bookTreeOutput($pseudo_tree);
         }
       }
       if ($book_menus) {
@@ -140,10 +152,10 @@ public function build() {
       $nid = $select->execute()->fetchField();
       // Only show the block if the user has view access for the top-level node.
       if ($nid) {
-        $tree = \Drupal::service('book.manager')->bookTreeAllData($node->book['bid'], $node->book);
+        $tree = $this->bookManager->bookTreeAllData($node->book['bid'], $node->book);
         // There should only be one element at the top level.
         $data = array_shift($tree);
-        $below = \Drupal::service('book.manager')->bookTreeOutput($data['below']);
+        $below = $this->bookManager->bookTreeOutput($data['below']);
         if (!empty($below)) {
           return $below;
         }