diff --git a/composer/Plugin/VendorHardening/FileSecurity.php b/composer/Plugin/VendorHardening/FileSecurity.php
index 6e89744beb..263314582a 100644
--- a/composer/Plugin/VendorHardening/FileSecurity.php
+++ b/composer/Plugin/VendorHardening/FileSecurity.php
@@ -28,7 +28,7 @@ class FileSecurity {
    *   TRUE if the file already exists or was created. FALSE otherwise.
    */
   public static function writeHtaccess($directory, $deny_public_access = TRUE, $force = FALSE) {
-    return self::writeFile($directory, '.htaccess', self::htaccessLines($deny_public_access), $force);
+    return self::writeFile($directory, '/.htaccess', self::htaccessLines($deny_public_access), $force);
   }
 
   /**
@@ -112,7 +112,7 @@ protected static function denyPublicAccess() {
    *   TRUE if the file already exists or was created. FALSE otherwise.
    */
   public static function writeWebConfig($directory, $force = FALSE) {
-    return self::writeFile($directory, 'web.config', self::webConfigLines(), $force);
+    return self::writeFile($directory, '/web.config', self::webConfigLines(), $force);
   }
 
   /**
@@ -154,9 +154,7 @@ protected static function writeFile($directory, $filename, $contents, $force) {
     if (file_exists($file_path) && !$force) {
       return TRUE;
     }
-    // Try to write the file. This can fail if concurrent requests are both
-    // trying to write a the same time.
-    if (@file_put_contents($file_path, $contents)) {
+    if (file_exists($directory) && is_writable($directory) && file_put_contents($file_path, $contents)) {
       return @chmod($file_path, 0444);
     }
     return FALSE;
diff --git a/core/lib/Drupal/Component/FileSecurity/FileSecurity.php b/core/lib/Drupal/Component/FileSecurity/FileSecurity.php
index 1b90afd8dd..d9996bbbca 100644
--- a/core/lib/Drupal/Component/FileSecurity/FileSecurity.php
+++ b/core/lib/Drupal/Component/FileSecurity/FileSecurity.php
@@ -26,7 +26,7 @@ class FileSecurity {
    *   TRUE if the file already exists or was created. FALSE otherwise.
    */
   public static function writeHtaccess($directory, $deny_public_access = TRUE, $force = FALSE) {
-    return self::writeFile($directory, '.htaccess', self::htaccessLines($deny_public_access), $force);
+    return self::writeFile($directory, '/.htaccess', self::htaccessLines($deny_public_access), $force);
   }
 
   /**
@@ -110,7 +110,7 @@ protected static function denyPublicAccess() {
    *   TRUE if the file already exists or was created. FALSE otherwise.
    */
   public static function writeWebConfig($directory, $force = FALSE) {
-    return self::writeFile($directory, 'web.config', self::webConfigLines(), $force);
+    return self::writeFile($directory, '/web.config', self::webConfigLines(), $force);
   }
 
   /**
@@ -152,12 +152,7 @@ protected static function writeFile($directory, $filename, $contents, $force) {
     if (file_exists($file_path) && !$force) {
       return TRUE;
     }
-    // Writing the file can fail if:
-    // - concurrent requests are both trying to write at the same time.
-    // - $directory does not exist or is not writable.
-    // Testing for these conditions introduces windows for concurrency issues to
-    // occur.
-    if (@file_put_contents($file_path, $contents)) {
+    if (file_exists($directory) && is_writable($directory) && file_put_contents($file_path, $contents)) {
       return @chmod($file_path, 0444);
     }
     return FALSE;
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index 3540a42ef0..09f8434370 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -193,9 +193,14 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
           }
         }
 
-        // Update the module handler in order to have the correct module list
-        // for the kernel update.
+        // Update the module handler in order to load the module's code.
+        // This allows the module to participate in hooks and its existence to
+        // be discovered by other modules.
+        // The current ModuleHandler instance is obsolete with the kernel
+        // rebuild below.
         $this->moduleHandler->setModuleList($module_filenames);
+        $this->moduleHandler->load($module);
+        module_load_install($module);
 
         // Clear the static cache of the "extension.list.module" service to pick
         // up the new module, since it merges the installation status of modules
@@ -205,10 +210,6 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
         // Update the kernel to include it.
         $this->updateKernel($module_filenames);
 
-        // Load the module's .module and .install files.
-        $this->moduleHandler->load($module);
-        module_load_install($module);
-
         // Replace the route provider service with a version that will rebuild
         // if routes used during installation. This ensures that a module's
         // routes are available during installation. This has to occur before
diff --git a/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php b/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php
index a6a736f5d9..1da76f31e1 100644
--- a/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php
+++ b/core/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php
@@ -25,6 +25,7 @@ class BlockConfigSchemaTest extends KernelTestBase {
     'block_content',
     'comment',
     'forum',
+    'history',
     'node',
     'statistics',
     // \Drupal\block\Entity\Block->preSave() calls system_region_list().
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php
index 1801de8780..379fb64a1c 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php
@@ -20,6 +20,7 @@ class MigrateBlockContentTranslationTest extends MigrateDrupal6TestBase {
     'block',
     'comment',
     'forum',
+    'history',
     'views',
     'block_content',
     'content_translation',
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php
index f39057308c..f397f10279 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php
@@ -26,6 +26,7 @@ class MigrateBlockTest extends MigrateDrupal6TestBase {
     'aggregator',
     'book',
     'forum',
+    'history',
     'path_alias',
     'statistics',
   ];
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
index 9789b19913..ac118cdb6c 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
@@ -23,6 +23,7 @@ class MigrateBlockContentTranslationTest extends MigrateDrupal7TestBase {
     'comment',
     'filter',
     'forum',
+    'history',
     'views',
     'block_content',
     'config_translation',
diff --git a/core/modules/comment/comment.info.yml b/core/modules/comment/comment.info.yml
index 12ff94b4f0..b7bb4ba84b 100644
--- a/core/modules/comment/comment.info.yml
+++ b/core/modules/comment/comment.info.yml
@@ -5,4 +5,5 @@ package: Core
 version: VERSION
 dependencies:
   - drupal:text
+  - drupal:history
 configure: comment.admin
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index db68f43cd8..d5c2d652d3 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -35,7 +35,10 @@
  * Comments changed after this time may be marked new, updated, or read,
  * depending on their state for the current user. Defaults to 30 days ago.
  *
- * @todo Remove when https://www.drupal.org/node/1029708 lands.
+ * @deprecated in drupal:8.9.0 and is removed from drupal:9.0.0.
+ *   No replacement is provided.
+ *
+ * @see https://www.drupal.org/node/1029708
  */
 define('COMMENT_NEW_LIMIT', REQUEST_TIME - 30 * 24 * 60 * 60);
 
diff --git a/core/modules/comment/comment.services.yml b/core/modules/comment/comment.services.yml
index 3595854ceb..cbd41a6a49 100644
--- a/core/modules/comment/comment.services.yml
+++ b/core/modules/comment/comment.services.yml
@@ -7,7 +7,7 @@ services:
 
   comment.manager:
     class: Drupal\comment\CommentManager
-    arguments: ['@entity_type.manager', '@config.factory', '@string_translation', '@module_handler', '@current_user', '@entity_field.manager', '@entity_display.repository']
+    arguments: ['@entity_type.manager', '@config.factory', '@string_translation', '@module_handler', '@current_user', '@entity_field.manager', '@entity_display.repository', '@history.repository']
 
   comment.statistics:
     class: Drupal\comment\CommentStatistics
@@ -21,4 +21,4 @@ services:
 
   comment.link_builder:
     class: Drupal\comment\CommentLinkBuilder
-    arguments: ['@current_user', '@comment.manager', '@module_handler', '@string_translation', '@entity_type.manager']
+    arguments: ['@current_user', '@comment.manager', '@module_handler', '@string_translation', '@entity_type.manager', '@history.repository']
diff --git a/core/modules/comment/src/CommentLinkBuilder.php b/core/modules/comment/src/CommentLinkBuilder.php
index 4725f88237..8608273c46 100644
--- a/core/modules/comment/src/CommentLinkBuilder.php
+++ b/core/modules/comment/src/CommentLinkBuilder.php
@@ -11,6 +11,7 @@
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\Url;
+use Drupal\history\HistoryRepositoryInterface;
 
 /**
  * Defines a class for building markup for comment links on a commented entity.
@@ -49,6 +50,13 @@ class CommentLinkBuilder implements CommentLinkBuilderInterface {
    */
   protected $entityTypeManager;
 
+  /**
+   * The history repository service.
+   *
+   * @var \Drupal\history\HistoryRepositoryInterface
+   */
+  protected $historyRepository;
+
   /**
    * Constructs a new CommentLinkBuilder object.
    *
@@ -62,13 +70,20 @@ class CommentLinkBuilder implements CommentLinkBuilderInterface {
    *   String translation service.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity type manager.
+   * @param \Drupal\history\HistoryRepositoryInterface $history_repository
+   *   The history repository service.
    */
-  public function __construct(AccountInterface $current_user, CommentManagerInterface $comment_manager, ModuleHandlerInterface $module_handler, TranslationInterface $string_translation, EntityTypeManagerInterface $entity_type_manager) {
+  public function __construct(AccountInterface $current_user, CommentManagerInterface $comment_manager, ModuleHandlerInterface $module_handler, TranslationInterface $string_translation, EntityTypeManagerInterface $entity_type_manager, HistoryRepositoryInterface $history_repository = NULL) {
     $this->currentUser = $current_user;
     $this->commentManager = $comment_manager;
     $this->moduleHandler = $module_handler;
     $this->stringTranslation = $string_translation;
     $this->entityTypeManager = $entity_type_manager;
+    if (!$history_repository) {
+      @trigger_error('Calling CommentLinkBuilder::__construct() without the $history_repository argument is deprecated in drupal:8.9.0. The $history_repository argument will be required in drupal:9.0.0. See https://www.drupal.org/node/2081585', E_USER_DEPRECATED);
+      $history_repository = \Drupal::service('history.repository');
+    }
+    $this->historyRepository = $history_repository;
   }
 
   /**
@@ -198,7 +213,8 @@ public function buildCommentedEntityLinks(FieldableEntityInterface $entity, arra
           $entity_links['comment__' . $field_name]['#attached']['library'][] = 'comment/drupal.node-new-comments-link';
           // Embed the metadata for the "X new comments" link (if any) on this
           // entity.
-          $entity_links['comment__' . $field_name]['#attached']['drupalSettings']['history']['lastReadTimestamps'][$entity->id()] = (int) history_read($entity->id());
+          $timestamps = $this->historyRepository->getLastViewed($entity->getEntityTypeId(), [$entity->id()], $this->currentUser);
+          $entity_links['comment__' . $field_name]['#attached']['drupalSettings']['history']['lastReadTimestamps'][$entity->id()] = (int) $timestamps[$entity->id()];
           $new_comments = $this->commentManager->getCountNewComments($entity);
           if ($new_comments > 0) {
             $page_number = $this->entityTypeManager
diff --git a/core/modules/comment/src/CommentManager.php b/core/modules/comment/src/CommentManager.php
index 41556aa759..85c349e558 100644
--- a/core/modules/comment/src/CommentManager.php
+++ b/core/modules/comment/src/CommentManager.php
@@ -16,6 +16,7 @@
 use Drupal\Core\Url;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\field\Entity\FieldConfig;
+use Drupal\history\HistoryRepositoryInterface;
 use Drupal\user\RoleInterface;
 use Drupal\user\UserInterface;
 
@@ -74,6 +75,13 @@ class CommentManager implements CommentManagerInterface {
    */
   protected $currentUser;
 
+  /**
+   * The history repository service.
+   *
+   * @var \Drupal\history\HistoryRepositoryInterface
+   */
+  protected $historyRepository;
+
   /**
    * Construct the CommentManager object.
    *
@@ -91,8 +99,10 @@ class CommentManager implements CommentManagerInterface {
    *   The entity field manager service.
    * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
    *   The entity display repository service.
+   * @param \Drupal\history\HistoryRepositoryInterface $history_repository
+   *   The history repository service.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, TranslationInterface $string_translation, ModuleHandlerInterface $module_handler, AccountInterface $current_user, EntityFieldManagerInterface $entity_field_manager, EntityDisplayRepositoryInterface $entity_display_repository) {
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, TranslationInterface $string_translation, ModuleHandlerInterface $module_handler, AccountInterface $current_user, EntityFieldManagerInterface $entity_field_manager, EntityDisplayRepositoryInterface $entity_display_repository, HistoryRepositoryInterface $history_repository = NULL) {
     $this->entityTypeManager = $entity_type_manager;
     $this->userConfig = $config_factory->get('user.settings');
     $this->stringTranslation = $string_translation;
@@ -100,6 +110,11 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Con
     $this->currentUser = $current_user;
     $this->entityFieldManager = $entity_field_manager;
     $this->entityDisplayRepository = $entity_display_repository;
+    if (!$history_repository) {
+      @trigger_error('Calling CommentManager::__construct() without the $history_repository argument is deprecated in drupal:8.9.0. The $history_repository argument will be required in drupal:9.0.0. See https://www.drupal.org/node/2081585', E_USER_DEPRECATED);
+      $history_repository = \Drupal::service('history.repository');
+    }
+    $this->historyRepository = $history_repository;
   }
 
   /**
@@ -201,20 +216,8 @@ public function getCountNewComments(EntityInterface $entity, $field_name = NULL,
     if ($this->currentUser->isAuthenticated() && $this->moduleHandler->moduleExists('history')) {
       // Retrieve the timestamp at which the current user last viewed this entity.
       if (!$timestamp) {
-        if ($entity->getEntityTypeId() == 'node') {
-          $timestamp = history_read($entity->id());
-        }
-        else {
-          $function = $entity->getEntityTypeId() . '_last_viewed';
-          if (function_exists($function)) {
-            $timestamp = $function($entity->id());
-          }
-          else {
-            // Default to 30 days ago.
-            // @todo Remove once https://www.drupal.org/node/1029708 lands.
-            $timestamp = COMMENT_NEW_LIMIT;
-          }
-        }
+        $timestamp = $this->historyRepository->getLastViewed($entity->getEntityTypeId(), [$entity->id()], $this->currentUser);
+        $timestamp = $timestamp[$entity->id()];
       }
       $timestamp = ($timestamp > HISTORY_READ_LIMIT ? $timestamp : HISTORY_READ_LIMIT);
 
diff --git a/core/modules/comment/src/Plugin/views/field/NodeNewComments.php b/core/modules/comment/src/Plugin/views/field/NodeNewComments.php
index 2555b7be88..3f7e3ee500 100644
--- a/core/modules/comment/src/Plugin/views/field/NodeNewComments.php
+++ b/core/modules/comment/src/Plugin/views/field/NodeNewComments.php
@@ -156,7 +156,7 @@ public function preRender(&$values) {
 
     if ($nids) {
       $result = $this->database->query("SELECT n.nid, COUNT(c.cid) as num_comments FROM {node} n INNER JOIN {comment_field_data} c ON n.nid = c.entity_id AND c.entity_type = 'node' AND c.default_langcode = 1
-        LEFT JOIN {history} h ON h.nid = n.nid AND h.uid = :h_uid WHERE n.nid IN ( :nids[] )
+        LEFT JOIN {history} h ON h.entity_id = n.nid AND h.uid = :h_uid WHERE n.nid IN ( :nids[] )
         AND c.changed > GREATEST(COALESCE(h.timestamp, :timestamp1), :timestamp2) AND c.status = :status GROUP BY n.nid", [
         ':status' => CommentInterface::PUBLISHED,
         ':h_uid' => $user->id(),
diff --git a/core/modules/comment/tests/src/Unit/CommentLinkBuilderTest.php b/core/modules/comment/tests/src/Unit/CommentLinkBuilderTest.php
index 64cdc6079e..7745dffa0c 100644
--- a/core/modules/comment/tests/src/Unit/CommentLinkBuilderTest.php
+++ b/core/modules/comment/tests/src/Unit/CommentLinkBuilderTest.php
@@ -6,6 +6,7 @@
 use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Url;
+use Drupal\history\HistoryRepositoryInterface;
 use Drupal\node\NodeInterface;
 use Drupal\Tests\Traits\Core\GeneratePermutationsTrait;
 use Drupal\Tests\UnitTestCase;
@@ -39,6 +40,13 @@ class CommentLinkBuilderTest extends UnitTestCase {
    */
   protected $entityTypeManager;
 
+  /**
+   * The history repository mock.
+   *
+   * @var \Drupal\history\HistoryRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $historyRepository;
+
   /**
    * Module handler mock.
    *
@@ -72,9 +80,10 @@ protected function setUp(): void {
     $this->commentManager = $this->createMock('\Drupal\comment\CommentManagerInterface');
     $this->stringTranslation = $this->getStringTranslationStub();
     $this->entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
+    $this->historyRepository = $this->createMock(HistoryRepositoryInterface::class);
     $this->moduleHandler = $this->createMock('\Drupal\Core\Extension\ModuleHandlerInterface');
     $this->currentUser = $this->createMock('\Drupal\Core\Session\AccountProxyInterface');
-    $this->commentLinkBuilder = new CommentLinkBuilder($this->currentUser, $this->commentManager, $this->moduleHandler, $this->stringTranslation, $this->entityTypeManager);
+    $this->commentLinkBuilder = new CommentLinkBuilder($this->currentUser, $this->commentManager, $this->moduleHandler, $this->stringTranslation, $this->entityTypeManager, $this->historyRepository);
     $this->commentManager->expects($this->any())
       ->method('getFields')
       ->with('node')
@@ -318,13 +327,3 @@ protected function getMockNode($has_field, $comment_status, $form_location, $com
   }
 
 }
-
-namespace Drupal\comment;
-
-if (!function_exists('history_read')) {
-
-  function history_read() {
-    return 0;
-  }
-
-}
diff --git a/core/modules/comment/tests/src/Unit/CommentManagerTest.php b/core/modules/comment/tests/src/Unit/CommentManagerTest.php
index cfc65d3af4..d2d4f875ee 100644
--- a/core/modules/comment/tests/src/Unit/CommentManagerTest.php
+++ b/core/modules/comment/tests/src/Unit/CommentManagerTest.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\history\HistoryRepositoryInterface;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -34,6 +35,7 @@ public function testGetFields() {
 
     $entity_field_manager = $this->createMock(EntityFieldManagerInterface::class);
     $entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
+    $history_repository = $this->createMock(HistoryRepositoryInterface::class);
 
     $entity_field_manager->expects($this->once())
       ->method('getFieldMapByFieldType')
@@ -49,6 +51,10 @@ public function testGetFields() {
       ->method('getDefinition')
       ->will($this->returnValue($entity_type));
 
+    $history_repository->expects($this->any())
+      ->method('getLastViewed')
+      ->willReturn(0);
+
     $comment_manager = new CommentManager(
       $entity_type_manager,
       $this->createMock('Drupal\Core\Config\ConfigFactoryInterface'),
@@ -56,7 +62,8 @@ public function testGetFields() {
       $this->createMock('Drupal\Core\Extension\ModuleHandlerInterface'),
       $this->createMock(AccountInterface::class),
       $entity_field_manager,
-      $this->prophesize(EntityDisplayRepositoryInterface::class)->reveal()
+      $this->prophesize(EntityDisplayRepositoryInterface::class)->reveal(),
+      $history_repository
     );
     $comment_fields = $comment_manager->getFields('node');
     $this->assertArrayHasKey('field_foobar', $comment_fields);
diff --git a/core/modules/forum/forum.services.yml b/core/modules/forum/forum.services.yml
index 6268c2cf8a..a83af7fea2 100644
--- a/core/modules/forum/forum.services.yml
+++ b/core/modules/forum/forum.services.yml
@@ -1,7 +1,7 @@
 services:
   forum_manager:
     class: Drupal\forum\ForumManager
-    arguments: ['@config.factory', '@entity_type.manager', '@database', '@string_translation', '@comment.manager', '@entity_field.manager']
+    arguments: ['@config.factory', '@entity_type.manager', '@database', '@string_translation', '@comment.manager', '@entity_field.manager', '@history.repository']
     tags:
       - { name: backend_overridable }
   forum.breadcrumb.node:
diff --git a/core/modules/forum/src/ForumManager.php b/core/modules/forum/src/ForumManager.php
index 4a7b0d460e..696ea5fef0 100644
--- a/core/modules/forum/src/ForumManager.php
+++ b/core/modules/forum/src/ForumManager.php
@@ -11,6 +11,7 @@
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\comment\CommentManagerInterface;
+use Drupal\history\HistoryRepositoryInterface;
 use Drupal\node\NodeInterface;
 
 /**
@@ -113,6 +114,13 @@ class ForumManager implements ForumManagerInterface {
    */
   protected $index;
 
+  /**
+   * The history repository service.
+   *
+   * @var \Drupal\history\HistoryRepositoryInterface
+   */
+  protected $historyRepository;
+
   /**
    * Constructs the forum manager service.
    *
@@ -128,14 +136,21 @@ class ForumManager implements ForumManagerInterface {
    *   The comment manager service.
    * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
    *   The entity field manager.
+   * @param \Drupal\history\HistoryRepositoryInterface $history_repository
+   *   The history repository service.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, Connection $connection, TranslationInterface $string_translation, CommentManagerInterface $comment_manager, EntityFieldManagerInterface $entity_field_manager) {
+  public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, Connection $connection, TranslationInterface $string_translation, CommentManagerInterface $comment_manager, EntityFieldManagerInterface $entity_field_manager, HistoryRepositoryInterface $history_repository = NULL) {
     $this->configFactory = $config_factory;
     $this->entityTypeManager = $entity_type_manager;
     $this->connection = $connection;
     $this->stringTranslation = $string_translation;
     $this->commentManager = $comment_manager;
     $this->entityFieldManager = $entity_field_manager;
+    if (!$history_repository) {
+      @trigger_error('Calling ForumManager::__construct() without the $history_repository argument is deprecated in drupal:8.9.0. The $history_repository argument will be required in drupal:9.0.0. See https://www.drupal.org/node/2081585', E_USER_DEPRECATED);
+      $history_repository = \Drupal::service('history.repository');
+    }
+    $this->historyRepository = $history_repository;
   }
 
   /**
@@ -318,16 +333,11 @@ protected function getTopicOrder($sortby) {
    *   previously viewed the node; otherwise HISTORY_READ_LIMIT.
    */
   protected function lastVisit($nid, AccountInterface $account) {
+    $this->history += $this->historyRepository->getLastViewed('node', [$nid], $account);
     if (empty($this->history[$nid])) {
-      $result = $this->connection->select('history', 'h')
-        ->fields('h', ['nid', 'timestamp'])
-        ->condition('uid', $account->id())
-        ->execute();
-      foreach ($result as $t) {
-        $this->history[$t->nid] = $t->timestamp > HISTORY_READ_LIMIT ? $t->timestamp : HISTORY_READ_LIMIT;
-      }
+      $this->history[$nid] = HISTORY_READ_LIMIT;
     }
-    return isset($this->history[$nid]) ? $this->history[$nid] : HISTORY_READ_LIMIT;
+    return $this->history[$nid];
   }
 
   /**
@@ -480,7 +490,7 @@ public function checkNodeType(NodeInterface $node) {
   public function unreadTopics($term, $uid) {
     $query = $this->connection->select('node_field_data', 'n');
     $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', [':tid' => $term]);
-    $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', [':uid' => $uid]);
+    $query->leftJoin('history', 'h', "n.nid = h.entity_id AND h.entity_type = 'node' AND h.uid = :uid", [':uid' => $uid]);
     $query->addExpression('COUNT(n.nid)', 'count');
     return $query
       ->condition('status', 1)
@@ -488,7 +498,7 @@ public function unreadTopics($term, $uid) {
       //   field language and just fall back to the default language.
       ->condition('n.default_langcode', 1)
       ->condition('n.created', HISTORY_READ_LIMIT, '>')
-      ->isNull('h.nid')
+      ->isNull('h.entity_id')
       ->addTag('node_access')
       ->execute()
       ->fetchField();
diff --git a/core/modules/forum/tests/src/Kernel/ForumValidationTest.php b/core/modules/forum/tests/src/Kernel/ForumValidationTest.php
index d7fef86c4b..7d33900837 100644
--- a/core/modules/forum/tests/src/Kernel/ForumValidationTest.php
+++ b/core/modules/forum/tests/src/Kernel/ForumValidationTest.php
@@ -23,6 +23,7 @@ class ForumValidationTest extends EntityKernelTestBase {
     'options',
     'comment',
     'taxonomy',
+    'history',
     'forum',
   ];
 
diff --git a/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumConfigsTest.php b/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumConfigsTest.php
index b68f2f0336..1de2038b2a 100644
--- a/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumConfigsTest.php
+++ b/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumConfigsTest.php
@@ -17,7 +17,7 @@ class MigrateForumConfigsTest extends MigrateDrupal6TestBase {
   /**
    * {@inheritdoc}
    */
-  protected static $modules = ['comment', 'forum', 'taxonomy'];
+  protected static $modules = ['comment', 'forum', 'history', 'taxonomy'];
 
   /**
    * {@inheritdoc}
diff --git a/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumTest.php b/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumTest.php
index 05c6df74b9..d76b530f83 100644
--- a/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumTest.php
+++ b/core/modules/forum/tests/src/Kernel/Migrate/d6/MigrateForumTest.php
@@ -22,6 +22,7 @@ class MigrateForumTest extends MigrateNodeTestBase {
   protected static $modules = [
     'comment',
     'forum',
+    'history',
     'menu_ui',
     'taxonomy',
   ];
diff --git a/core/modules/forum/tests/src/Kernel/Migrate/d7/MigrateForumSettingsTest.php b/core/modules/forum/tests/src/Kernel/Migrate/d7/MigrateForumSettingsTest.php
index 6b1ab64b3c..d440b75b8c 100644
--- a/core/modules/forum/tests/src/Kernel/Migrate/d7/MigrateForumSettingsTest.php
+++ b/core/modules/forum/tests/src/Kernel/Migrate/d7/MigrateForumSettingsTest.php
@@ -19,6 +19,7 @@ class MigrateForumSettingsTest extends MigrateDrupal7TestBase {
     'text',
     'node',
     'taxonomy',
+    'history',
     'forum',
   ];
 
diff --git a/core/modules/forum/tests/src/Unit/ForumManagerTest.php b/core/modules/forum/tests/src/Unit/ForumManagerTest.php
index 969bb71219..abb9f1bc07 100644
--- a/core/modules/forum/tests/src/Unit/ForumManagerTest.php
+++ b/core/modules/forum/tests/src/Unit/ForumManagerTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\history\HistoryRepository;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -56,6 +57,10 @@ public function testGetIndex() {
       ->disableOriginalConstructor()
       ->getMock();
 
+    $history_repository = $this->getMockBuilder(HistoryRepository::class)
+      ->disableOriginalConstructor()
+      ->getMock();
+
     $comment_manager = $this->getMockBuilder('\Drupal\comment\CommentManagerInterface')
       ->disableOriginalConstructor()
       ->getMock();
@@ -69,6 +74,7 @@ public function testGetIndex() {
         $translation_manager,
         $comment_manager,
         $entity_field_manager,
+        $history_repository,
       ])
       ->getMock();
 
diff --git a/core/modules/history/history.install b/core/modules/history/history.install
index 3020d8bc24..046ed7cb37 100644
--- a/core/modules/history/history.install
+++ b/core/modules/history/history.install
@@ -5,21 +5,30 @@
  * Installation functions for History module.
  */
 
+use Drupal\Core\Entity\EntityTypeInterface;
+
 /**
  * Implements hook_schema().
  */
 function history_schema() {
   $schema['history'] = [
-    'description' => 'A record of which {users} have read which {node}s.',
+    'description' => 'A record of which {users} have read which entities.',
     'fields' => [
       'uid' => [
-        'description' => 'The {users}.uid that read the {node} nid.',
+        'description' => 'The {users}.uid that read the {history}.entity_id entity.',
         'type' => 'int',
         'not null' => TRUE,
         'default' => 0,
       ],
-      'nid' => [
-        'description' => 'The {node}.nid that was read.',
+      'entity_type' => [
+        'description' => 'The type of the entity that was read.',
+        'type' => 'varchar_ascii',
+        'not null' => TRUE,
+        'default' => '',
+        'length' => EntityTypeInterface::ID_MAX_LENGTH,
+      ],
+      'entity_id' => [
+        'description' => 'The ID of the entity that was read.',
         'type' => 'int',
         'unsigned' => TRUE,
         'not null' => TRUE,
@@ -32,9 +41,9 @@ function history_schema() {
         'default' => 0,
       ],
     ],
-    'primary key' => ['uid', 'nid'],
+    'primary key' => ['entity_type', 'uid', 'entity_id'],
     'indexes' => [
-      'nid' => ['nid'],
+      'history_entity' => ['entity_type', 'entity_id'],
     ],
   ];
 
@@ -47,3 +56,70 @@ function history_schema() {
 function history_update_last_removed() {
   return 8101;
 }
+
+/**
+ * Change {history}.nid to {history}.entity_id and add {history}.entity_type.
+ */
+function history_update_8801() {
+  $database = Database::getConnection();
+  $schema = $database->schema();
+  $schema->dropPrimaryKey('history');
+  $schema->dropIndex('history', 'nid');
+  $schema->changeField('history', 'nid', 'entity_id', [
+    'description' => 'The ID of the entity that was read.',
+    'type' => 'int',
+    'unsigned' => TRUE,
+    'not null' => TRUE,
+    'default' => 0,
+  ]);
+  $schema->addField('history', 'entity_type', [
+    'description' => 'The type of the entity that was read.',
+    'type' => 'varchar_ascii',
+    'not null' => TRUE,
+    'default' => '',
+    'length' => EntityTypeInterface::ID_MAX_LENGTH,
+  ]);
+
+  // Set default value for {history}.entity_type.
+  $database->update('history')
+    ->fields(['entity_type' => 'node'])
+    ->execute();
+
+  $schema->addPrimaryKey('history', ['entity_type', 'uid', 'entity_id']);
+  $spec = [
+    'description' => 'A record of which {users} have read which entities.',
+    'fields' => [
+      'uid' => [
+        'description' => 'The {users}.uid that read the {history}.entity_id entity.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ],
+      'entity_type' => [
+        'description' => 'The type of the entity that was read.',
+        'type' => 'varchar_ascii',
+        'not null' => TRUE,
+        'default' => '',
+        'length' => EntityTypeInterface::ID_MAX_LENGTH,
+      ],
+      'entity_id' => [
+        'description' => 'The ID of the entity that was read.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ],
+      'timestamp' => [
+        'description' => 'The Unix timestamp at which the read occurred.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ],
+    ],
+    'primary key' => ['entity_type', 'uid', 'entity_id'],
+    'indexes' => [
+      'history_entity' => ['entity_type', 'entity_id'],
+    ],
+  ];
+  $schema->addIndex('history', 'history_entity', ['entity_type', 'entity_id'], $spec);
+}
diff --git a/core/modules/history/history.module b/core/modules/history/history.module
index a6d0f27fc9..fdb50af146 100644
--- a/core/modules/history/history.module
+++ b/core/modules/history/history.module
@@ -13,6 +13,8 @@
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\node\Entity\Node;
 use Drupal\user\UserInterface;
 
 /**
@@ -44,10 +46,12 @@ function history_help($route_name, RouteMatchInterface $route_match) {
  * @return int
  *   If a node has been previously viewed by the user, the timestamp in seconds
  *   of when the last view occurred; otherwise, zero.
+ *
+ * @deprecated Use \Drupal\history\HistoryRepositoryInterface::getLastViewed() instead.
  */
 function history_read($nid) {
-  $history = history_read_multiple([$nid]);
-  return $history[$nid];
+  $timestamps = \Drupal::service('history.repository')->getLastViewed('node', [$nid], \Drupal::currentUser());
+  return $timestamps[$nid];
 }
 
 /**
@@ -60,37 +64,11 @@ function history_read($nid) {
  *   Array of timestamps keyed by node ID. If a node has been previously viewed
  *   by the user, the timestamp in seconds of when the last view occurred;
  *   otherwise, zero.
+ *
+ * @deprecated Use \Drupal\history\HistoryRepositoryInterface::getLastViewed() instead.
  */
 function history_read_multiple($nids) {
-  $history = &drupal_static(__FUNCTION__, []);
-
-  $return = [];
-
-  $nodes_to_read = [];
-  foreach ($nids as $nid) {
-    if (isset($history[$nid])) {
-      $return[$nid] = $history[$nid];
-    }
-    else {
-      // Initialize value if current user has not viewed the node.
-      $nodes_to_read[$nid] = 0;
-    }
-  }
-
-  if (empty($nodes_to_read)) {
-    return $return;
-  }
-
-  $result = \Drupal::database()->query('SELECT nid, timestamp FROM {history} WHERE uid = :uid AND nid IN ( :nids[] )', [
-    ':uid' => \Drupal::currentUser()->id(),
-    ':nids[]' => array_keys($nodes_to_read),
-  ]);
-  foreach ($result as $row) {
-    $nodes_to_read[$row->nid] = (int) $row->timestamp;
-  }
-  $history += $nodes_to_read;
-
-  return $return + $nodes_to_read;
+  return \Drupal::service('history.repository')->getLastViewed('node', $nids, \Drupal::currentUser());
 }
 
 /**
@@ -98,37 +76,26 @@ function history_read_multiple($nids) {
  *
  * @param $nid
  *   The node ID that has been read.
- * @param $account
+ * @param \Drupal\Core\Session\AccountInterface $account
  *   (optional) The user account to update the history for. Defaults to the
  *   current user.
+ *
+ * @deprecated Use \Drupal\history\HistoryRepositoryInterface::updateLastViewed() instead.
  */
-function history_write($nid, $account = NULL) {
+function history_write($nid, AccountInterface $account = NULL) {
 
   if (!isset($account)) {
     $account = \Drupal::currentUser();
   }
-
-  if ($account->isAuthenticated()) {
-    \Drupal::database()->merge('history')
-      ->keys([
-        'uid' => $account->id(),
-        'nid' => $nid,
-      ])
-      ->fields(['timestamp' => REQUEST_TIME])
-      ->execute();
-    // Update static cache.
-    $history = &drupal_static('history_read_multiple', []);
-    $history[$nid] = REQUEST_TIME;
-  }
+  $node = Node::load($nid);
+  \Drupal::service('history.repository')->updateLastViewed($node, $account);
 }
 
 /**
  * Implements hook_cron().
  */
 function history_cron() {
-  \Drupal::database()->delete('history')
-    ->condition('timestamp', HISTORY_READ_LIMIT, '<')
-    ->execute();
+  \Drupal::service('history.repository')->purge();
 }
 
 /**
@@ -153,9 +120,7 @@ function history_node_view_alter(array &$build, EntityInterface $node, EntityVie
  * Implements hook_ENTITY_TYPE_delete() for node entities.
  */
 function history_node_delete(EntityInterface $node) {
-  \Drupal::database()->delete('history')
-    ->condition('nid', $node->id())
-    ->execute();
+  \Drupal::service('history.repository')->deleteByEntity($node);
 }
 
 /**
@@ -164,9 +129,7 @@ function history_node_delete(EntityInterface $node) {
 function history_user_cancel($edit, UserInterface $account, $method) {
   switch ($method) {
     case 'user_cancel_reassign':
-      \Drupal::database()->delete('history')
-        ->condition('uid', $account->id())
-        ->execute();
+      \Drupal::service('history.repository')->deleteByUser($account);
       break;
   }
 }
@@ -175,7 +138,5 @@ function history_user_cancel($edit, UserInterface $account, $method) {
  * Implements hook_ENTITY_TYPE_delete() for user entities.
  */
 function history_user_delete($account) {
-  \Drupal::database()->delete('history')
-    ->condition('uid', $account->id())
-    ->execute();
+  \Drupal::service('history.repository')->deleteByUser($account);
 }
diff --git a/core/modules/history/history.services.yml b/core/modules/history/history.services.yml
new file mode 100644
index 0000000000..db37fcc6bb
--- /dev/null
+++ b/core/modules/history/history.services.yml
@@ -0,0 +1,6 @@
+services:
+  history.repository:
+    class: Drupal\history\HistoryRepository
+    arguments: ['@database', '@datetime.time', '@entity.memory_cache']
+    tags:
+      - { name: backend_overridable }
diff --git a/core/modules/history/history.views.inc b/core/modules/history/history.views.inc
index 60c95cd868..4df607754f 100644
--- a/core/modules/history/history.views.inc
+++ b/core/modules/history/history.views.inc
@@ -22,8 +22,9 @@ function history_views_data() {
     'node_field_data' => [
       'table' => 'history',
       'left_field' => 'nid',
-      'field' => 'nid',
+      'field' => 'entity_id',
       'extra' => [
+        ['field' => 'entity_type', 'value' => 'node'],
         ['field' => 'uid', 'value' => '***CURRENT_USER***', 'numeric' => TRUE],
       ],
     ],
diff --git a/core/modules/history/src/Controller/HistoryController.php b/core/modules/history/src/Controller/HistoryController.php
index c6a4834903..218a164d6c 100644
--- a/core/modules/history/src/Controller/HistoryController.php
+++ b/core/modules/history/src/Controller/HistoryController.php
@@ -2,11 +2,13 @@
 
 namespace Drupal\history\Controller;
 
+use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\history\HistoryRepositoryInterface;
 use Drupal\node\NodeInterface;
 
 /**
@@ -14,13 +16,39 @@
  */
 class HistoryController extends ControllerBase {
 
+  /**
+   * The history repository service.
+   *
+   * @var \Drupal\history\HistoryRepositoryInterface
+   */
+  protected $historyRepository;
+
+  /**
+   * Constructs a HistoryController object.
+   *
+   * @param \Drupal\history\HistoryRepositoryInterface $history_repository
+   *   The history repository service.
+   */
+  public function __construct(HistoryRepositoryInterface $history_repository) {
+    $this->historyRepository = $history_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('history.repository')
+    );
+  }
+
   /**
    * Returns a set of nodes' last read timestamps.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   The request of the page.
    *
-   * @return Symfony\Component\HttpFoundation\JsonResponse
+   * @return \Symfony\Component\HttpFoundation\JsonResponse
    *   The JSON response.
    */
   public function getNodeReadTimestamps(Request $request) {
@@ -32,7 +60,7 @@ public function getNodeReadTimestamps(Request $request) {
     if (!isset($nids)) {
       throw new NotFoundHttpException();
     }
-    return new JsonResponse(history_read_multiple($nids));
+    return new JsonResponse($this->historyRepository->getLastViewed('node', $nids, $this->currentUser()));
   }
 
   /**
@@ -42,6 +70,8 @@ public function getNodeReadTimestamps(Request $request) {
    *   The request of the page.
    * @param \Drupal\node\NodeInterface $node
    *   The node whose "last read" timestamp should be updated.
+   *
+   * @return \Symfony\Component\HttpFoundation\JsonResponse
    */
   public function readNode(Request $request, NodeInterface $node) {
     if ($this->currentUser()->isAnonymous()) {
@@ -49,9 +79,10 @@ public function readNode(Request $request, NodeInterface $node) {
     }
 
     // Update the history table, stating that this user viewed this node.
-    history_write($node->id());
+    $this->historyRepository->updateLastViewed($node, $this->currentUser());
 
-    return new JsonResponse((int) history_read($node->id()));
+    $timestamps = $this->historyRepository->getLastViewed('node', [$node->id()], $this->currentUser());
+    return new JsonResponse($timestamps[$node->id()]);
   }
 
 }
diff --git a/core/modules/history/src/HistoryRenderCallback.php b/core/modules/history/src/HistoryRenderCallback.php
index c6a92eda68..982709d397 100644
--- a/core/modules/history/src/HistoryRenderCallback.php
+++ b/core/modules/history/src/HistoryRenderCallback.php
@@ -20,7 +20,8 @@ class HistoryRenderCallback implements RenderCallbackInterface {
    */
   public static function lazyBuilder($node_id) {
     $element = [];
-    $element['#attached']['drupalSettings']['history']['lastReadTimestamps'][$node_id] = (int) history_read($node_id);
+    $timestamps = \Drupal::service('history.repository')->getLastViewed('node', [$node_id], \Drupal::currentUser());
+    $element['#attached']['drupalSettings']['history']['lastReadTimestamps'][$node_id] = (int) $timestamps[$node_id];
     return $element;
   }
 
diff --git a/core/modules/history/src/HistoryRepository.php b/core/modules/history/src/HistoryRepository.php
new file mode 100644
index 0000000000..b2339c3655
--- /dev/null
+++ b/core/modules/history/src/HistoryRepository.php
@@ -0,0 +1,189 @@
+<?php
+
+namespace Drupal\history;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
+
+/**
+ * Provides history repository service.
+ */
+class HistoryRepository implements HistoryRepositoryInterface {
+
+  /**
+   * An array of history IDs keyed by entity type and entity id.
+   *
+   * @var array
+   */
+  protected $history = [];
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * The memory cache.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $memoryCache;
+
+  /**
+   * Constructs the history repository service.
+   *
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The time service.
+   * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
+   *   The memory cache.
+   */
+  public function __construct(Connection $connection, TimeInterface $time, MemoryCacheInterface $memory_cache) {
+    $this->connection = $connection;
+    $this->time = $time;
+    $this->memoryCache = $memory_cache;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLastViewed($entity_type, array $entity_ids, AccountInterface $account) {
+    $entities = [];
+    $entities_to_read = [];
+    foreach ($entity_ids as $entity_id) {
+      // Load from the cache.
+      $cached = $this->memoryCache->get(
+        $this->buildCacheId($account->id(), $entity_type, $entity_id
+      ));
+      if ($cached) {
+        $entities[$entity_id] = $cached->data;
+      }
+      else {
+        $entities_to_read[$entity_id] = 0;
+      }
+    }
+
+    if (empty($entities_to_read)) {
+      return $entities;
+    }
+
+    $result = $this->connection->select('history', 'h')
+      ->fields('h', ['entity_id', 'timestamp'])
+      ->condition('uid', $account->id())
+      ->condition('entity_type', $entity_type)
+      ->condition('entity_id', array_keys($entities_to_read), 'IN')
+      ->execute();
+
+    foreach ($result as $row) {
+      $timestamp = (int) $row->timestamp;
+      $this->memoryCache->set(
+        $this->buildCacheId($account->id(), $entity_type, $row->entity_id),
+        $timestamp,
+        Cache::PERMANENT,
+        $this->getCacheTags($account->id(), $row->entity_id)
+      );
+      $entities_to_read[$row->entity_id] = $timestamp;
+    }
+
+    return $entities + $entities_to_read;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateLastViewed(EntityInterface $entity, AccountInterface $account) {
+    if ($account->isAuthenticated()) {
+      $this->connection->merge('history')
+        ->keys([
+          'uid' => $account->id(),
+          'entity_id' => $entity->id(),
+          'entity_type' => $entity->getEntityTypeId(),
+        ])
+        ->fields(['timestamp' => $this->time->getRequestTime()])
+        ->execute();
+      // Update cached value.
+      $this->memoryCache->set(
+        $this->buildCacheId($account->id(), $entity->getEntityTypeId(), $entity->id()),
+        $this->time->getRequestTime(), Cache::PERMANENT,
+        $this->getCacheTags($account->id(), $entity->id())
+      );
+    }
+  }
+
+  /**
+   * Builds the cache ID for the history timestamp.
+   *
+   * @param int $uid
+   *   The User ID.
+   * @param string $entity_type
+   *   The entity type.
+   * @param int $entity_id
+   *   The entity ID.
+   *
+   * @return string
+   *   Cache ID that can be passed to the cache backend.
+   */
+  protected function buildCacheId($uid, $entity_type, $entity_id) {
+    return implode(':', ['history', $uid, $entity_type, $entity_id]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags($uid, $entity_id) {
+    return [
+      'history',
+      "history:user:{$uid}",
+      "history:entity:{$entity_id}",
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function purge() {
+    $this->connection->delete('history')
+      ->condition('timestamp', HISTORY_READ_LIMIT, '<')
+      ->execute();
+    // Clean static cache.
+    Cache::invalidateTags(['history']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteByUser(AccountInterface $account) {
+    $this->connection->delete('history')
+      ->condition('uid', $account->id())
+      ->execute();
+    // Clean static cache.
+    Cache::invalidateTags(["history:user:{$account->id()}"]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteByEntity(EntityInterface $entity) {
+    $this->connection->delete('history')
+      ->condition('entity_id', $entity->id())
+      ->condition('entity_type', $entity->getEntityTypeId())
+      ->execute();
+    // Clean static cache.
+    Cache::invalidateTags(["history:entity:{$entity->id()}"]);
+  }
+
+}
diff --git a/core/modules/history/src/HistoryRepositoryInterface.php b/core/modules/history/src/HistoryRepositoryInterface.php
new file mode 100644
index 0000000000..de74570a00
--- /dev/null
+++ b/core/modules/history/src/HistoryRepositoryInterface.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\history\HistoryRepositoryInterface.
+ */
+
+namespace Drupal\history;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines an interface to store and retrieve a last view timestamp of entities.
+ */
+interface HistoryRepositoryInterface {
+
+  /**
+   * Retrieves the timestamp for the current user's last view of the entities.
+   *
+   * @param string $entity_type
+   *   The entity type.
+   * @param array $entity_ids
+   *   The entity IDs.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user account to get the history for.
+   *
+   * @return array
+   *   Array of timestamps keyed by entity ID. If a entity has been previously
+   *   viewed by the user, the timestamp in seconds of when the last view
+   *   occurred; otherwise, zero.
+   */
+  public function getLastViewed($entity_type, array $entity_ids, AccountInterface $account);
+
+  /**
+   * Updates 'last viewed' timestamp of the entity for the user account.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity that history should be updated.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user account to update the history for.
+   */
+  public function updateLastViewed(EntityInterface $entity, AccountInterface $account);
+
+  /**
+   * Gets an array of cache tags for the history timestamp.
+   *
+   * @param int $uid
+   *   The User ID.
+   * @param int $entity_id
+   *   The entity ID.
+   *
+   * @return string[]
+   *   An array of cache tags based on the current view.
+   */
+  public function getCacheTags($uid, $entity_id);
+
+  /**
+   * Purges outdated history.
+   */
+  public function purge();
+
+  /**
+   * Deletes the history for the given user account.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user account to purge history.
+   */
+  public function deleteByUser(AccountInterface $account);
+
+  /**
+   * Deletes the history for the given entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity that history should be deleted.
+   */
+  public function deleteByEntity(EntityInterface $entity);
+
+}
diff --git a/core/modules/history/tests/src/Functional/Update/HistoryUpdateTest.php b/core/modules/history/tests/src/Functional/Update/HistoryUpdateTest.php
new file mode 100644
index 0000000000..b28d043d44
--- /dev/null
+++ b/core/modules/history/tests/src/Functional/Update/HistoryUpdateTest.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\Tests\history\Functional\Update;
+
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+
+/**
+ * Tests update functions for the History module.
+ *
+ * @group Update
+ * @group legacy
+ */
+class HistoryUpdateTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['history'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
+    ];
+  }
+
+  /**
+   * Tests changing nid to entity_id and adding an entity_type field to the history table.
+   *
+   * @see history_update_8801()
+   */
+  public function testUpdateHookN() {
+    $database = \Drupal::database();
+    $schema = $database->schema();
+
+    // Run updates.
+    $this->runUpdates();
+
+    // Ensure fields were added.
+    $this->assertTrue($schema->fieldExists('history', 'entity_type'));
+    $this->assertTrue($schema->fieldExists('history', 'entity_id'));
+    // Ensure field was removed.
+    $this->assertFalse($schema->fieldExists('history', 'nid'));
+
+    $this->assertTrue($schema->indexExists('history', 'history_entity'));
+    $this->assertFalse($schema->indexExists('history', 'nid'));
+
+    $entries = $database->select('history')
+      ->fields('history')
+      ->condition('entity_type', 'node', '!=')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertEquals(0, $entries);
+  }
+
+}
diff --git a/core/modules/history/tests/src/Kernel/Views/HistoryTimestampTest.php b/core/modules/history/tests/src/Kernel/Views/HistoryTimestampTest.php
index ddcb064c0f..a422d41a74 100644
--- a/core/modules/history/tests/src/Kernel/Views/HistoryTimestampTest.php
+++ b/core/modules/history/tests/src/Kernel/Views/HistoryTimestampTest.php
@@ -72,14 +72,16 @@ public function testHandlers() {
     $connection->insert('history')
       ->fields([
         'uid' => $account->id(),
-        'nid' => $nodes[0]->id(),
+        'entity_id' => $nodes[0]->id(),
+        'entity_type' => 'node',
         'timestamp' => REQUEST_TIME - 100,
       ])->execute();
 
     $connection->insert('history')
       ->fields([
         'uid' => $account->id(),
-        'nid' => $nodes[1]->id(),
+        'entity_id' => $nodes[1]->id(),
+        'entity_type' => 'node',
         'timestamp' => REQUEST_TIME + 100,
       ])->execute();
 
diff --git a/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php b/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php
index 10ed8755cf..248cdc7bee 100644
--- a/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php
+++ b/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php
@@ -39,6 +39,7 @@ class MigrationPluginListTest extends KernelTestBase {
     'file',
     'filter',
     'forum',
+    'history',
     'image',
     'language',
     'locale',
diff --git a/core/modules/system/tests/modules/module_autoload_test/module_autoload_test.module b/core/modules/system/tests/modules/module_autoload_test/module_autoload_test.module
deleted file mode 100644
index cf6983bc7f..0000000000
--- a/core/modules/system/tests/modules/module_autoload_test/module_autoload_test.module
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module.
- */
-
-use Drupal\module_autoload_test\SomeClass;
-
-define('MODULE_AUTOLOAD_TEST_CONSTANT', SomeClass::TEST);
diff --git a/core/modules/system/tests/modules/module_autoload_test/src/SomeClass.php b/core/modules/system/tests/modules/module_autoload_test/src/SomeClass.php
index 65529f23dc..11993b71f4 100644
--- a/core/modules/system/tests/modules/module_autoload_test/src/SomeClass.php
+++ b/core/modules/system/tests/modules/module_autoload_test/src/SomeClass.php
@@ -4,8 +4,6 @@
 
 class SomeClass {
 
-  const TEST = '\Drupal\module_autoload_test\SomeClass::TEST';
-
   public function testMethod() {
     return 'Drupal\\module_autoload_test\\SomeClass::testMethod() was invoked.';
   }
diff --git a/core/modules/system/tests/src/Functional/Module/ClassLoaderTest.php b/core/modules/system/tests/src/Functional/Module/ClassLoaderTest.php
index c22247b63f..cb855e34a9 100644
--- a/core/modules/system/tests/src/Functional/Module/ClassLoaderTest.php
+++ b/core/modules/system/tests/src/Functional/Module/ClassLoaderTest.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\Tests\system\Functional\Module;
 
-use Drupal\module_autoload_test\SomeClass;
 use Drupal\Tests\BrowserTestBase;
 
 /**
@@ -97,19 +96,4 @@ public function testMultipleModules() {
     $this->assertTrue(\Drupal::moduleHandler()->moduleExists('module_install_class_loader_test2'), 'The module_install_class_loader_test2 module has been installed.');
   }
 
-  /**
-   * Tests that .module files can use class constants in main section.
-   */
-  public function testAutoloadFromModuleFile() {
-    $this->assertFalse(defined('MODULE_AUTOLOAD_TEST_CONSTANT'));
-    $this->drupalLogin($this->rootUser);
-    $edit = [
-      "modules[module_autoload_test][enable]" => TRUE,
-    ];
-    $this->drupalPostForm('admin/modules', $edit, t('Install'));
-    $this->assertSession()->statusCodeEquals(200);
-    $this->resetAll();
-    $this->assertSame(SomeClass::TEST, MODULE_AUTOLOAD_TEST_CONSTANT);
-  }
-
 }
