diff --git a/core/core.services.yml b/core/core.services.yml
index de408ad..e11d8bc 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -575,7 +575,7 @@ services:
       - { name: event_subscriber }
   entity.definition_update_manager:
     class: Drupal\Core\Entity\EntityDefinitionUpdateManager
-    arguments: ['@entity.manager', '@entity.last_installed_schema.repository']
+    arguments: ['@entity.manager', '@entity.last_installed_schema.repository', '@entity_type.listener']
   entity.last_installed_schema.repository:
     class: Drupal\Core\Entity\EntityLastInstalledSchemaRepository
     arguments: ['@keyvalue']
diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
index c5d0abd..f551fdd 100644
--- a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
@@ -29,14 +29,23 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
   protected $entityLastInstalledSchemaRepository;
 
   /**
+   * The entity type listener service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeListener
+   */
+  protected $entityTypeListener;
+
+  /**
    * Constructs a new EntityDefinitionUpdateManager.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
    *   The entity manager.
    * @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository
    *   The last installed schema repository service.
+   * @param \Drupal\Core\Entity\EntityTypeListener $entity_type_listener
+   *   The entity type listener service.
    */
-  public function __construct(EntityManagerInterface $entity_manager, EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository = NULL) {
+  public function __construct(EntityManagerInterface $entity_manager, EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository = NULL, $entity_type_listener = NULL) {
     $this->entityManager = $entity_manager;
 
     if (!isset($entity_last_installed_schema_repository)) {
@@ -44,6 +53,12 @@ public function __construct(EntityManagerInterface $entity_manager, EntityLastIn
       $entity_last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository');
     }
     $this->entityLastInstalledSchemaRepository = $entity_last_installed_schema_repository;
+
+    if (!isset($entity_type_listener)) {
+      @trigger_error('The $entity_type_listener parameter was added in Drupal 8.6.x and will be required in 9.0.0. See https://www.drupal.org/node/2973262.', E_USER_DEPRECATED);
+      $entity_type_listener = \Drupal::service('entity_type.listener');
+    }
+    $this->entityTypeListener = $entity_type_listener;
   }
 
   /**
@@ -154,7 +169,7 @@ public function getEntityTypes() {
    */
   public function installEntityType(EntityTypeInterface $entity_type) {
     $this->entityManager->clearCachedDefinitions();
-    $this->entityManager->onEntityTypeCreate($entity_type);
+    $this->entityTypeListener->onEntityTypeCreate($entity_type);
   }
 
   /**
@@ -163,7 +178,7 @@ public function installEntityType(EntityTypeInterface $entity_type) {
   public function updateEntityType(EntityTypeInterface $entity_type) {
     $original = $this->getEntityType($entity_type->id());
     $this->entityManager->clearCachedDefinitions();
-    $this->entityManager->onEntityTypeUpdate($entity_type, $original);
+    $this->entityTypeListener->onEntityTypeUpdate($entity_type, $original);
   }
 
   /**
@@ -171,7 +186,21 @@ public function updateEntityType(EntityTypeInterface $entity_type) {
    */
   public function uninstallEntityType(EntityTypeInterface $entity_type) {
     $this->entityManager->clearCachedDefinitions();
-    $this->entityManager->onEntityTypeDelete($entity_type);
+    $this->entityTypeListener->onEntityTypeDelete($entity_type);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateEntityTypeSchema(EntityTypeInterface $entity_type, array $field_storage_definitions, array &$sandbox = NULL) {
+    $original = $this->getEntityType($entity_type->id());
+
+    if ($this->requiresEntityDataMigration($entity_type, $original) && $sandbox === NULL) {
+      throw new \InvalidArgumentException('The entity schema update for the ' . $entity_type->id() . ' entity type requires a data migration.');
+    }
+
+    $original_field_storage_definitions = $original_storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type->id());
+    $this->entityTypeListener->onEntityTypeSchemaUpdate($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox);
   }
 
   /**
@@ -373,4 +402,20 @@ protected function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInter
     return ($storage instanceof DynamicallyFieldableEntityStorageSchemaInterface) && $storage->requiresFieldStorageSchemaChanges($storage_definition, $original);
   }
 
+  /**
+   * Checks if existing data would be lost if the schema changes were applied.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The updated entity type definition.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $original
+   *   The original entity type definition.
+   *
+   * @return bool
+   *   TRUE if data migration is required, FALSE otherwise.
+   */
+  protected function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    $storage = $this->entityManager->getStorage($entity_type->id());
+    return ($storage instanceof EntityStorageSchemaInterface) && $storage->requiresEntityDataMigration($entity_type, $original);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php
index a219f3c..3494b60 100644
--- a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php
@@ -145,6 +145,20 @@ public function updateEntityType(EntityTypeInterface $entity_type);
   public function uninstallEntityType(EntityTypeInterface $entity_type);
 
   /**
+   * Applies any schema change performed to the passed entity type definition.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The updated entity type definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions
+   *   The updated field storage definitions, including possibly new ones.
+   * @param array &$sandbox
+   *   (optional) A sandbox array provided by a hook_update_N() implementation
+   *   or a Batch API callback. If the entity schema update requires a data
+   *   migration, this parameter is mandatory. Defaults to NULL.
+   */
+  public function updateEntityTypeSchema(EntityTypeInterface $entity_type, array $field_storage_definitions, array &$sandbox = NULL);
+
+  /**
    * Returns a field storage definition ready to be manipulated.
    *
    * When needing to apply updates to existing field storage definitions, this
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeListener.php b/core/lib/Drupal/Core/Entity/EntityTypeListener.php
index 4dc1e9c..6706fef 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeListener.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeListener.php
@@ -9,7 +9,7 @@
  *
  * @see \Drupal\Core\Entity\EntityTypeEvents
  */
-class EntityTypeListener implements EntityTypeListenerInterface {
+class EntityTypeListener implements EntityTypeListenerInterface, EntityTypeSchemaListenerInterface {
 
   /**
    * The entity type manager.
@@ -115,4 +115,25 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
     $this->entityLastInstalledSchemaRepository->deleteLastInstalledDefinition($entity_type_id);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeSchemaUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) {
+    $entity_type_id = $entity_type->id();
+
+    // @todo Forward this to all interested handlers, not only storage, once
+    //   iterating handlers is possible: https://www.drupal.org/node/2332857.
+    $storage = $this->entityTypeManager->createHandlerInstance($entity_type->getStorageClass(), $entity_type);
+    if ($storage instanceof EntityTypeSchemaListenerInterface) {
+      $storage->onEntityTypeSchemaUpdate($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox);
+    }
+
+    $this->eventDispatcher->dispatch(EntityTypeEvents::UPDATE, new EntityTypeEvent($entity_type, $original));
+
+    $this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type);
+    if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
+      $this->entityLastInstalledSchemaRepository->setLastInstalledFieldStorageDefinitions($entity_type_id, $field_storage_definitions);
+    }
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeSchemaListenerInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeSchemaListenerInterface.php
new file mode 100644
index 0000000..d053104
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/EntityTypeSchemaListenerInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\Core\Entity;
+
+/**
+ * Defines an interface for reacting to entity type schema changes.
+ */
+interface EntityTypeSchemaListenerInterface {
+
+  /**
+   * Reacts to the update of the entity type schema.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The updated entity type definition.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $original
+   *   The original entity type definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions
+   *   The updated field storage definitions, including possibly new ones.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $original_field_storage_definitions
+   *   The original field storage definitions.
+   * @param array &$sandbox
+   *   (optional) A sandbox array provided by a hook_update_N() implementation
+   *   or a Batch API callback. If the entity schema update requires a data
+   *   migration, this parameter is mandatory. Defaults to NULL.
+   */
+  public function onEntityTypeSchemaUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL);
+
+}
