diff --git a/core/core.services.yml b/core/core.services.yml
index f8c428f..3c53c42 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -285,6 +285,9 @@ services:
     parent: container.trait
     tags:
       - { name: plugin_manager_cache_clear }
+  entity.schema.manager:
+    class: Drupal\Core\Entity\Schema\EntitySchemaManager
+    arguments: ['@entity.manager', '@state']
   entity.form_builder:
     class: Drupal\Core\Entity\EntityFormBuilder
     arguments: ['@entity.manager', '@form_builder']
diff --git a/core/includes/update.inc b/core/includes/update.inc
index f9a88db..a601796 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -10,14 +10,8 @@
 
 use Drupal\Component\Graph\Graph;
 use Drupal\Component\Utility\String;
-use Drupal\Core\Config\FileStorage;
-use Drupal\Core\Config\ConfigException;
-use Drupal\Core\DrupalKernel;
-use Drupal\Core\Page\DefaultHtmlPageRenderer;
+use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Utility\Error;
-use Drupal\Component\Uuid\Uuid;
-use Drupal\Component\Utility\NestedArray;
-use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Disables any extensions that are incompatible with the current core version.
@@ -258,6 +252,33 @@ function update_do_one($module, $number, $dependency_map, &$context) {
 }
 
 /**
+ * Performs entity schema updates.
+ *
+ * @param $module
+ *   The module whose update will be run.
+ * @param $number
+ *   The update number to run.
+ * @param $context
+ *   The batch context array.
+ */
+function update_entity_schema($module, $number, &$context) {
+  try {
+    \Drupal::service('entity.schema.manager')->applyChanges();
+  }
+  catch (EntityStorageException $e) {
+    watchdog_exception('update', $e);
+    $variables = Error::decodeException($e);
+    unset($variables['backtrace']);
+    // The exception message is run through
+    // \Drupal\Component\Utility\String::checkPlain() by
+    // \Drupal\Core\Utility\Error::decodeException().
+    $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables));
+    $context['results'][$module][$number] = $ret;
+    $context['results']['#abort'][] = 'update_entity_schema';
+  }
+}
+
+/**
  * Returns a list of all the pending database updates.
  *
  * @return
diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php
index e3acce9..7d6ce96 100644
--- a/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -17,6 +17,8 @@
 use Drupal\Core\Entity\Exception\AmbiguousEntityClassException;
 use Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException;
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Plugin\DefaultPluginManager;
@@ -967,6 +969,9 @@ public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
     if ($storage instanceof EntityTypeListenerInterface) {
       $storage->onEntityTypeCreate($entity_type);
     }
+
+    // Keep the schema manager synchronized with the current state.
+    $this->schemaManager()->onEntityTypeCreate($entity_type);
   }
 
   /**
@@ -979,6 +984,9 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
     if ($storage instanceof EntityTypeListenerInterface) {
       $storage->onEntityTypeUpdate($entity_type, $original);
     }
+
+    // Keep the schema manager synchronized with the current state.
+    $this->schemaManager()->onEntityTypeUpdate($entity_type, $original);
   }
 
   /**
@@ -991,6 +999,57 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
     if ($storage instanceof EntityTypeListenerInterface) {
       $storage->onEntityTypeDelete($entity_type);
     }
+
+    // Keep the schema manager synchronized with the current state.
+    $this->schemaManager()->onEntityTypeDelete($entity_type);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
+    // @todo Forward this to all interested handlers, not only storage, once
+    //   iterating handlers is possible: https://www.drupal.org/node/2332857.
+    $storage = $this->getStorage($storage_definition->getTargetEntityTypeId());
+    if ($storage instanceof FieldStorageDefinitionListenerInterface) {
+      $storage->onFieldStorageDefinitionCreate($storage_definition);
+    }
+    $this->clearCachedFieldDefinitions();
+
+    // Keep the schema manager synchronized with the current state.
+    $this->schemaManager()->onFieldStorageDefinitionCreate($storage_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    // @todo Forward this to all interested handlers, not only storage, once
+    //   iterating handlers is possible: https://www.drupal.org/node/2332857.
+    $storage = $this->getStorage($storage_definition->getTargetEntityTypeId());
+    if ($storage instanceof FieldStorageDefinitionListenerInterface) {
+      $storage->onFieldStorageDefinitionUpdate($storage_definition, $original);
+    }
+    $this->clearCachedFieldDefinitions();
+
+    // Keep the schema manager synchronized with the current state.
+    $this->schemaManager()->onFieldStorageDefinitionUpdate($storage_definition, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
+    // @todo Forward this to all interested handlers, not only storage, once
+    //   iterating handlers is possible: https://www.drupal.org/node/2332857.
+    $storage = $this->getStorage($storage_definition->getTargetEntityTypeId());
+    if ($storage instanceof FieldStorageDefinitionListenerInterface) {
+      $storage->onFieldStorageDefinitionDelete($storage_definition);
+    }
+    $this->clearCachedFieldDefinitions();
+
+    // Keep the schema manager synchronized with the current state.
+    $this->schemaManager()->onFieldStorageDefinitionDelete($storage_definition);
   }
 
   /**
@@ -1047,4 +1106,13 @@ public function onBundleDelete($entity_type_id, $bundle) {
     $this->clearCachedFieldDefinitions();
   }
 
+  /**
+   * Returns the entity schema manager service.
+   *
+   * @return \Drupal\Core\Entity\Schema\EntitySchemaManagerInterface
+   */
+  protected function schemaManager() {
+    return $this->container->get('entity.schema.manager');
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php
index 8d3f9f4..7dd7d23 100644
--- a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php
@@ -8,11 +8,12 @@
 namespace Drupal\Core\Entity;
 
 use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
 
 /**
  * Provides an interface for entity type managers.
  */
-interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListenerInterface {
+interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListenerInterface, FieldStorageDefinitionListenerInterface {
 
   /**
    * Builds a list of entity type labels suitable for a Form API options list.
diff --git a/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php
index 54e2bff..0149ec3 100644
--- a/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php
+++ b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php
@@ -7,7 +7,10 @@
 
 namespace Drupal\Core\Entity\Exception;
 
+use Drupal\Core\Entity\EntityStorageException;
+
 /**
  * Exception thrown when a storage definition update is forbidden.
  */
-class FieldStorageDefinitionUpdateForbiddenException extends \Exception { }
+class FieldStorageDefinitionUpdateForbiddenException extends EntityStorageException {
+}
diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
index 2d695c3..aaab687 100644
--- a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
@@ -9,42 +9,9 @@
 
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
 
-interface FieldableEntityStorageInterface extends EntityStorageInterface {
-
-  /**
-   * Reacts to the creation of a field storage definition.
-   *
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
-   *   The definition being created.
-   */
-  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition);
-
-  /**
-   * Reacts to the update of a field storage definition.
-   *
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
-   *   The field being updated.
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
-   *   The original storage definition; i.e., the definition before the update.
-   *
-   * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
-   *   Thrown when the update to the field is forbidden.
-   */
-  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
-
-  /**
-   * Reacts to the deletion of a field storage definition.
-   *
-   * Stored values should not be wiped at once, but marked as 'deleted' so that
-   * they can go through a proper purge process later on.
-   *
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
-   *   The field being deleted.
-   *
-   * @see purgeFieldData()
-   */
-  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition);
+interface FieldableEntityStorageInterface extends EntityStorageInterface, FieldStorageDefinitionListenerInterface {
 
   /**
    * Reacts to the creation of a field.
@@ -59,7 +26,7 @@ public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definiti
    *
    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
    *   The field definition being updated.
-   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $original
    *   The original field definition; i.e., the definition before the update.
    */
   public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original);
diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntityStorageSchemaInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntityStorageSchemaInterface.php
new file mode 100644
index 0000000..c8bcd8c
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntityStorageSchemaInterface.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\ContentEntityStorageSchemaInterface.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
+
+/**
+ * Defines an interface for handling the storage schema of content entities.
+ */
+interface ContentEntityStorageSchemaInterface extends EntityStorageSchemaInterface, FieldStorageDefinitionListenerInterface {
+
+  /**
+   * Checks whether the definition changes imply field schema changes.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The updated field storage definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original field storage definition.
+   *
+   * @return bool
+   *   TRUE if the changes imply field schema changes, FALSE otherwise.
+   */
+  public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
+
+  /**
+   * Checks whether the field storage definition changes imply a data migration.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The updated field storage definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original field storage definition.
+   *
+   * @return bool
+   *   TRUE if the changes imply a data migration, FALSE otherwise.
+   */
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php
deleted file mode 100644
index 51001bd..0000000
--- a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Entity\Schema\EntitySchemaHandlerInterface.
- */
-
-namespace Drupal\Core\Entity\Schema;
-use Drupal\Core\Entity\EntityTypeListenerInterface;
-
-/**
- * Defines an interface for handling the storage schema of entities.
- */
-interface EntitySchemaHandlerInterface extends EntityTypeListenerInterface {
-}
diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaManager.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaManager.php
new file mode 100644
index 0000000..4d5cdc6
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaManager.php
@@ -0,0 +1,373 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\EntitySchemaManager.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Component\Utility\String;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\State\StateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Manages entity schema changes.
+ */
+class EntitySchemaManager implements EntitySchemaManagerInterface {
+  use StringTranslationTrait;
+
+  /**
+   * Indicates that a definition has just been created.
+   *
+   * @var int
+   */
+  const DEFINITION_CREATED = 1;
+
+  /**
+   * Indicates that a definition has changes.
+   *
+   * @var int
+   */
+  const DEFINITION_UPDATED = 2;
+
+  /**
+   * Indicates that a definition has just been deleted.
+   *
+   * @var int
+   */
+  const DEFINITION_DELETED = 3;
+
+  /**
+   * The entity manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   * @param StateInterface $state
+   */
+  public function __construct(EntityManagerInterface $entity_manager, StateInterface $state) {
+    $this->entityManager = $entity_manager;
+    $this->state = $state;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasChanges() {
+    return (bool) $this->getChangeList();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangeSummary() {
+    $summary = array();
+
+    foreach ($this->getChangeList() as $entity_type_id => $change_list) {
+      // Process entity type definition changes.
+      if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) {
+        $definition = $this->entityManager->getDefinition($entity_type_id);
+        $summary[$entity_type_id][] = $this->t('The %entity_type entity type has schema changes.', array('%entity_type' => $definition->getLabel()));
+      }
+
+      // Process field storage definition changes.
+      if (!empty($change_list['field_storage_definitions'])) {
+        $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
+        $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+
+        foreach ($change_list['field_storage_definitions'] as $field_name => $change) {
+          $definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : $original_storage_definitions[$field_name];
+          $args = array('%field_name' => $definition->getLabel());
+
+          switch ($change) {
+            case static::DEFINITION_CREATED:
+              $summary[$entity_type_id][] = $this->t('The %field_name field has been created.', $args);
+              break;
+
+            case static::DEFINITION_UPDATED:
+              $summary[$entity_type_id][] = $this->t('The %field_name field has schema changes.', $args);
+              break;
+
+            case static::DEFINITION_DELETED:
+              $summary[$entity_type_id][] = $this->t('The %field_name field has been deleted.', $args);
+              break;
+          }
+        }
+      }
+    }
+
+    return $summary;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyChanges() {
+    foreach ($this->getChangeList() as $entity_type_id => $change_list) {
+      // We do not allow any kind of schema change that would imply a data
+      // migration.
+      if (empty($change_list['data_migration'])) {
+        // Process entity type definition changes.
+        if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) {
+          $entity_type = $this->entityManager->getDefinition($entity_type_id);
+          $this->entityManager->onEntityTypeUpdate($entity_type, $this->loadEntityTypeDefinition($entity_type_id));
+        }
+
+        // Process field storage definition changes.
+        if (!empty($change_list['field_storage_definitions'])) {
+          $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
+          $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+
+          foreach ($change_list['field_storage_definitions'] as $field_name => $change) {
+            switch ($change) {
+              case static::DEFINITION_CREATED:
+                $this->entityManager->onFieldStorageDefinitionCreate($storage_definitions[$field_name]);
+                break;
+
+              case static::DEFINITION_UPDATED:
+                $this->entityManager->onFieldStorageDefinitionUpdate($storage_definitions[$field_name], $original_storage_definitions[$field_name]);
+                break;
+
+              case static::DEFINITION_DELETED:
+                $this->entityManager->onFieldStorageDefinitionDelete($storage_definitions[$field_name]);
+                break;
+            }
+          }
+        }
+      }
+      else {
+        $args = array('@entity_type_id' => $entity_type_id);
+        $message = String::format('Changes for the @entity_type_id entity type involve a data migration and cannot be applied.', $args);
+        throw new EntityStorageException($message);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
+    $this->saveEntityTypeDefinition($entity_type);
+    if ($entity_type->isFieldable()) {
+      $entity_type_id = $entity_type->id();
+      $this->saveFieldStorageDefinitions($entity_type_id, $this->entityManager->getFieldStorageDefinitions($entity_type_id));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    $this->saveEntityTypeDefinition($entity_type);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+    $this->deleteEntityTypeDefinition($entity_type_id);
+    // Ensure we delete any data concerning this entity type. It might have
+    // switched from fieldable to non-fieldable during its life cycle.
+    $this->deleteFieldStorageDefinitions($entity_type_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
+    $entity_type_id = $storage_definition->getTargetEntityTypeId();
+    // Update our field storage definitions.
+    $definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+    $definitions[$storage_definition->getName()] = $storage_definition;
+    $this->saveFieldStorageDefinitions($entity_type_id, $definitions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    $entity_type_id = $storage_definition->getTargetEntityTypeId();
+    // Update our field storage definitions.
+    $definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+    $definitions[$storage_definition->getName()] = $storage_definition;
+    $this->saveFieldStorageDefinitions($entity_type_id, $definitions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
+    $entity_type_id = $storage_definition->getTargetEntityTypeId();
+    // Update our field storage definitions.
+    $definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+    unset($definitions[$storage_definition->getName()]);
+    $this->saveFieldStorageDefinitions($entity_type_id, $definitions);
+  }
+
+  /**
+   * Returns a list of changes to entity type and field storage definitions.
+   *
+   * This implementation currently only tracks schema changes for content
+   * entities, because the storage of configuration entities does not depend on
+   * their schema.
+   *
+   * @return array
+   *   An associative array keyed by entity type id of change descriptors. Every
+   *   entry is an associative array with the following optional keys:
+   *   - entity_type: a scalar having only the DEFINITION_UPDATED value.
+   *   - field_storage_definitions: an associative array keyed by field name of
+   *     scalars having one value among:
+   *     - DEFINITION_CREATED
+   *     - DEFINITION_UPDATED
+   *     - DEFINITION_DELETED
+   *   - data_migration: boolean indicating whether the changes imply a data
+   *     migration.
+   */
+  protected function getChangeList() {
+    $this->entityManager->clearCachedDefinitions();
+    $change_list = array();
+    $definitions = array_filter($this->entityManager->getDefinitions(), function($definition) { return $definition instanceof ContentEntityTypeInterface; });
+    $entity_type_ids = isset($entity_type_id) ? array($entity_type_id) : array_keys($definitions);
+
+    foreach ($entity_type_ids as $entity_type_id) {
+      $definition = $definitions[$entity_type_id];
+      $storage = $this->entityManager->getStorage($entity_type_id);
+
+      if ($definition instanceof ContentEntityTypeInterface && $storage instanceof ContentEntityStorageSchemaInterface) {
+        // Check whether there are changes in the entity type definition that
+        // would affect entity schema.
+        $original = $this->loadEntityTypeDefinition($entity_type_id);
+        if ($storage->requiresEntitySchemaChanges($definition, $original)) {
+          $change_list[$entity_type_id]['entity_type'] = static::DEFINITION_UPDATED;
+          if ($storage->requiresEntityDataMigration($definition, $original)) {
+            $change_list[$entity_type_id]['data_migration'] = TRUE;
+          }
+        }
+
+        // Check whether there are changes in the field storage definitions that
+        // would affect entity schema. We skip definitions with custom storage
+        // as they do not affect entity schema.
+        if ($definition->isFieldable()) {
+          $field_changes = array();
+          $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id);
+          $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
+
+          // Detect created field storage definitions.
+          $created = array_filter(array_diff_key($storage_definitions, $original_storage_definitions), function(FieldStorageDefinitionInterface $definition) { return !$definition->hasCustomStorage(); });
+          $field_changes = array_merge($field_changes, array_map(function() { return static::DEFINITION_CREATED; }, $created));
+
+          // Detect deleted field storage definitions.
+          $deleted = array_filter(array_diff_key($original_storage_definitions, $storage_definitions), function(FieldStorageDefinitionInterface $definition) { return !$definition->hasCustomStorage(); });
+          $field_changes = array_merge($field_changes, array_map(function() { return static::DEFINITION_DELETED; }, $deleted));
+
+          // Now compare field storage definitions.
+          foreach (array_intersect_key($storage_definitions, $original_storage_definitions) as $field_name => $definition) {
+            /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $definition */
+            if (!$definition->hasCustomStorage()) {
+              $original = $this->loadFieldStorageDefinitions($definition->getTargetEntityTypeId())[$definition->getName()];
+              if ($storage->requiresFieldSchemaChanges($definition, $original)) {
+                $field_changes[$field_name] = static::DEFINITION_UPDATED;
+                if ($storage->requiresFieldDataMigration($definition, $original)) {
+                  $change_list[$entity_type_id]['data_migration'] = TRUE;
+                }
+              }
+            }
+          }
+
+          if ($field_changes) {
+            $change_list[$entity_type_id]['field_storage_definitions'] = $field_changes;
+          }
+        }
+      }
+    }
+
+    return array_filter($change_list);
+  }
+
+  /**
+   * Returns the specified stored entity type definition.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeInterface
+   *   A stored entity type definition.
+   */
+  protected function loadEntityTypeDefinition($entity_type_id) {
+    return $this->state->get('entity.schema.manager.' . $entity_type_id . '.entity_type');
+  }
+
+  /**
+   * Stores the specified stored entity type definition.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   */
+  protected function saveEntityTypeDefinition(EntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+    $this->state->set('entity.schema.manager.' . $entity_type_id . '.entity_type', $entity_type);
+  }
+
+  /**
+   * Deletes the specified stored entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type definition identifier.
+   */
+  protected function deleteEntityTypeDefinition($entity_type_id) {
+    $this->state->delete('entity.schema.manager.' . $entity_type_id . '.entity_type');
+  }
+
+  /**
+   * Returns the stored field storage definitions for the specified entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   *
+   * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
+   *   An array of field storage definitions.
+   */
+  protected function loadFieldStorageDefinitions($entity_type_id) {
+    return $this->state->get('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions');
+  }
+
+  /**
+   * Stores the field storage definitions for the specified entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type identifier.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
+   *   An array of field storage definitions.
+   */
+  protected function saveFieldStorageDefinitions($entity_type_id, array $storage_definitions) {
+    $this->state->set('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions', $storage_definitions);
+  }
+
+  /**
+   * Deletes the stored field storage definitions for the specified entity type.
+   *
+   * @param string $entity_type_id
+   *   The entity type definition identifier.
+   */
+  protected function deleteFieldStorageDefinitions($entity_type_id) {
+    $this->state->delete('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions');
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaManagerInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaManagerInterface.php
new file mode 100644
index 0000000..303bf04
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaManagerInterface.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\EntitySchemaManagerInterface.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+
+use Drupal\Core\Entity\EntityTypeListenerInterface;
+use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
+
+/**
+ * Defines an interface for managing entity schema changes.
+ */
+interface EntitySchemaManagerInterface extends EntityTypeListenerInterface, FieldStorageDefinitionListenerInterface {
+
+  /**
+   * Returns whether there are any schema changes that need to be applied.
+   *
+   * @return bool
+   *   TRUE if there are changes.
+   */
+  public function hasChanges();
+
+  /**
+   * Returns a human readable summary of the detected changes.
+   *
+   * @return array
+   *   An associative array keyed by entity type id. Each entry is an array of
+   *   human-readable strings, each describing a change.
+   */
+  public function getChangeSummary();
+
+  /**
+   * Applies all the detected valid changes.
+   *
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   *   This exception is thrown if a change cannot be applied without
+   *   unacceptable data loss. In such a case, the site administrator needs to
+   *   apply some other process, such as a custom update function or a
+   *   migration via the Migrate module.
+   */
+  public function applyChanges();
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php
new file mode 100644
index 0000000..2cd257b
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface.
+ */
+
+namespace Drupal\Core\Entity\Schema;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeListenerInterface;
+
+/**
+ * Defines an interface for handling the storage schema of entities.
+ */
+interface EntityStorageSchemaInterface extends EntityTypeListenerInterface {
+
+  /**
+   * Checks whether the definition changes imply entity schema changes.
+   *
+   * @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 the changes imply entity schema changes, FALSE otherwise.
+   */
+  public function requiresEntitySchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original);
+
+  /**
+   * Checks whether the entity type definition changes imply a data migration.
+   *
+   * @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 the changes imply a data migration, FALSE otherwise.
+   */
+  public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original);
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index 1814564..e978554 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -20,6 +20,7 @@
 use Drupal\Core\Entity\EntityTypeListenerInterface;
 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\Entity\Schema\ContentEntityStorageSchemaInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Language\LanguageInterface;
@@ -39,7 +40,7 @@
  *
  * @ingroup entity_api
  */
-class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, EntityTypeListenerInterface {
+class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, ContentEntityStorageSchemaInterface {
 
   /**
    * The mapping of field columns to SQL tables.
@@ -109,7 +110,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
   /**
    * The entity schema handler.
    *
-   * @var \Drupal\Core\Entity\Schema\EntitySchemaHandlerInterface
+   * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
    */
   protected $schemaHandler;
 
@@ -1366,6 +1367,34 @@ protected function usesDedicatedTable(FieldStorageDefinitionInterface $definitio
   /**
    * {@inheritdoc}
    */
+  public function requiresEntitySchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    return $this->schemaHandler()->requiresEntitySchemaChanges($entity_type, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    return $this->schemaHandler()->requiresFieldSchemaChanges($storage_definition, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    return $this->schemaHandler()->requiresEntityDataMigration($entity_type, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    return $this->schemaHandler()->requiresFieldDataMigration($storage_definition, $original);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
     $this->schemaHandler()->onEntityTypeCreate($entity_type);
   }
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
index 32c83c7..66b1473 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -7,16 +7,19 @@
 
 namespace Drupal\Core\Entity\Sql;
 
+use Drupal\Component\Utility\String;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\Schema\EntitySchemaHandlerInterface;
+use Drupal\Core\Entity\Schema\ContentEntityStorageSchemaInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Defines a schema handler that supports revisionable, translatable entities.
  */
-class SqlContentEntityStorageSchema implements EntitySchemaHandlerInterface {
+class SqlContentEntityStorageSchema implements ContentEntityStorageSchemaInterface {
 
   /**
    * The entity type this schema builder is responsible for.
@@ -75,7 +78,66 @@ public function __construct(EntityManagerInterface $entity_manager, ContentEntit
   /**
    * {@inheritdoc}
    */
+  public function requiresEntitySchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    return
+      $entity_type->getStorageClass() != $original->getStorageClass() ||
+      $entity_type->getKeys() != $original->getKeys() ||
+      $entity_type->isRevisionable() != $original->isRevisionable() ||
+      $entity_type->isTranslatable() != $original->isTranslatable();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    return
+      $storage_definition->hasCustomStorage() != $original->hasCustomStorage() ||
+      $storage_definition->getSchema() != $original->getSchema() ||
+      $storage_definition->isRevisionable() != $original->isRevisionable() ||
+      $storage_definition->isTranslatable() != $original->isTranslatable();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
+    // If we're updating from NULL storage, then there's no stored data that
+    // requires migration.
+    $original_storage_class = $original->getStorageClass();
+    $null_storage_class = 'Drupal\Core\Entity\ContentEntityNullStorage';
+    if ($original_storage_class == $null_storage_class || is_subclass_of($original_storage_class, $null_storage_class)) {
+      return FALSE;
+    }
+
+    return
+      // If the original storage class is different, then there might be
+      // existing entities in that storage even if the new storage's base
+      // table is empty.
+      $entity_type->getStorageClass() != $original_storage_class ||
+      !$this->tableIsEmpty($this->storage->getBaseTable());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    // If the base table is empty, there are no entities, and therefore, no
+    // field data that we care about preserving.
+    // @todo We might be returning TRUE here in cases where it would be safe
+    //   to return FALSE (for example, if the field is in a dedicated table
+    //   and that table is empty), and thereby preventing automatic updates
+    //   that should be possible, but determining that requires refactoring
+    //   SqlContentEntityStorage::_fieldSqlSchema(), and in the meantime,
+    //   it's safer to return false positives than false negatives:
+    //   https://www.drupal.org/node/1498720.
+    return !$this->tableIsEmpty($this->storage->getBaseTable());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
+    $this->checkEntityType($entity_type);
     $schema_handler = $this->database->schema();
     $schema = $this->getEntitySchema($entity_type, TRUE);
     foreach ($schema as $table_name => $table_schema) {
@@ -89,11 +151,39 @@ public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
    * {@inheritdoc}
    */
   public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
-    // @todo Implement proper updates: https://www.drupal.org/node/1498720.
-    //   Meanwhile, treat a change from non-SQL storage to SQL storage as
-    //   identical to creation with respect to SQL schema handling.
-    if (!is_subclass_of($original->getStorageClass(), '\Drupal\Core\Entity\Sql\SqlEntityStorageInterface')) {
-      $this->onEntityTypeCreate($entity_type);
+    $this->checkEntityType($entity_type);
+    $this->checkEntityType($original);
+
+    // If no schema changes are needed, we don't need to do anything.
+    if (!$this->requiresEntitySchemaChanges($entity_type, $original)) {
+      return;
+    }
+
+    // If we have no data just recreate the entity schema from scratch.
+    if (!$this->requiresEntityDataMigration($entity_type, $original)) {
+      if ($this->database->supportsTransactionalDDL()) {
+        // If the database supports transactional DDL, we can go ahead and rely
+        // on it. If not, we will have to rollback manually if something fails.
+        $transaction = $this->database->startTransaction();
+      }
+      try {
+        $this->onEntityTypeDelete($original);
+        $this->onEntityTypeCreate($entity_type);
+      }
+      catch (\Exception $e) {
+        if ($this->database->supportsTransactionalDDL()) {
+          $transaction->rollback();
+        }
+        else {
+          // Recreate original schema.
+          $this->onEntityTypeCreate($original);
+        }
+        throw $e;
+      }
+    }
+    // Otherwise, throw an exception.
+    else {
+      throw new EntityStorageException(String::format('The SQL storage cannot change the schema for an existing entity type with data.'));
     }
   }
 
@@ -101,9 +191,9 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
    * {@inheritdoc}
    */
   public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
+    $this->checkEntityType($entity_type);
     $schema_handler = $this->database->schema();
-    $schema = $this->getEntitySchema($entity_type, TRUE);
-    foreach ($schema as $table_name => $table_schema) {
+    foreach ($this->getEntitySchemaTables() as $table_name) {
       if ($schema_handler->tableExists($table_name)) {
         $schema_handler->dropTable($table_name);
       }
@@ -111,6 +201,51 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
+    // @todo Move implementation from
+    //   SqlContentEntityStorage::onFieldStorageDefinitionCreate()
+    //   into here: https://www.drupal.org/node/1498720
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    // @todo Move implementation from
+    //   SqlContentEntityStorage::onFieldStorageDefinitionUpdate()
+    //   into here: https://www.drupal.org/node/1498720
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
+    // @todo Move implementation from
+    //   SqlContentEntityStorage::onFieldStorageDefinitionDelete()
+    //   into here: https://www.drupal.org/node/1498720
+  }
+
+  /**
+   * Checks that we are dealing with the correct entity type.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type to be checked.
+   *
+   * @return bool
+   *   TRUE if the entity type matches the current one.
+   *
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   */
+  protected function checkEntityType(EntityTypeInterface $entity_type) {
+    if ($entity_type->id() != $this->entityType->id()) {
+      throw new EntityStorageException(String::format('Unsupported entity type @id', array('@id' => $entity_type->id())));
+    }
+    return TRUE;
+  }
+
+  /**
    * Returns the entity schema for the specified entity type.
    *
    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
@@ -128,7 +263,7 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
 
     if (!isset($this->schema[$entity_type_id]) || $reset) {
       // Initialize the table schema.
-      $tables = $this->getTables();
+      $tables = $this->getEntitySchemaTables();
       $schema[$tables['base_table']] = $this->initializeBaseTable();
       if (isset($tables['revision_table'])) {
         $schema[$tables['revision_table']] = $this->initializeRevisionTable();
@@ -180,7 +315,7 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
    * @return array
    *   A list of entity type tables, keyed by table key.
    */
-  protected function getTables() {
+  protected function getEntitySchemaTables() {
     return array_filter(array(
       'base_table' => $this->storage->getBaseTable(),
       'revision_table' => $this->storage->getRevisionTable(),
@@ -619,4 +754,24 @@ protected function getEntityIndexName($index) {
     return $this->entityType->id() . '__' . $index;
   }
 
+  /**
+   * Checks whether a database table is non-existent or empty.
+   *
+   * Empty tables can be dropped and recreated without data loss.
+   *
+   * @param string $table_name
+   *   The database table to check.
+   *
+   * @return bool
+   *   TRUE if the table is empty, FALSE otherwise.
+   */
+  protected function tableIsEmpty($table_name) {
+    return !$this->database->schema()->tableExists($table_name) ||
+      !$this->database->select($table_name)
+        ->countQuery()
+        ->range(0, 1)
+        ->execute()
+        ->fetchField();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionListenerInterface.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListenerInterface.php
new file mode 100644
index 0000000..fcea2e4
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListenerInterface.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Field\FieldStorageDefinitionListenerInterface.
+ */
+
+namespace Drupal\Core\Field;
+
+/**
+ * Defines an interface for reacting to field storage definition creation, deletion, and updates.
+ *
+ * @todo Convert to Symfony events: https://www.drupal.org/node/2332935
+ */
+interface FieldStorageDefinitionListenerInterface {
+
+  /**
+   * Reacts to the creation of a field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The definition being created.
+   */
+  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition);
+
+  /**
+   * Reacts to the update of a field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field being updated.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original storage definition; i.e., the definition before the update.
+   *
+   * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException
+   *   Thrown when the update to the field is forbidden.
+   */
+  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
+
+  /**
+   * Reacts to the deletion of a field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field being deleted.
+   */
+  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition);
+
+}
diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php
index f6953b3..128ac01 100644
--- a/core/modules/field/src/Entity/FieldStorageConfig.php
+++ b/core/modules/field/src/Entity/FieldStorageConfig.php
@@ -290,8 +290,8 @@ protected function preSaveNew(EntityStorageInterface $storage) {
     // definition is passed to the various hooks and written to config.
      $this->settings += $field_type_manager->getDefaultSettings($this->type);
 
-    // Notify the entity storage.
-    $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionCreate($this);
+    // Notify the entity manager.
+    $entity_manager->onFieldStorageDefinitionCreate($this);
   }
 
   /**
@@ -334,19 +334,16 @@ protected function preSaveUpdated(EntityStorageInterface $storage) {
     // invokes hook_field_storage_config_update_forbid().
     $module_handler->invokeAll('field_storage_config_update_forbid', array($this, $this->original));
 
-    // Notify the storage. The controller can reject the definition
+    // Notify the entity manager. A listener can reject the definition
     // update as invalid by raising an exception, which stops execution before
     // the definition is written to config.
-    $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionUpdate($this, $this->original);
+    $entity_manager->onFieldStorageDefinitionUpdate($this, $this->original);
   }
 
   /**
    * {@inheritdoc}
    */
   public function postSave(EntityStorageInterface $storage, $update = TRUE) {
-    // Clear the cache.
-    \Drupal::entityManager()->clearCachedFieldDefinitions();
-
     if ($update) {
       // Invalidate the render cache for all affected entities.
       $entity_manager = \Drupal::entityManager();
@@ -406,13 +403,10 @@ public static function postDelete(EntityStorageInterface $storage, array $fields
     // Notify the storage.
     foreach ($fields as $field) {
       if (!$field->deleted) {
-        \Drupal::entityManager()->getStorage($field->entity_type)->onFieldStorageDefinitionDelete($field);
+        \Drupal::entityManager()->onFieldStorageDefinitionDelete($field);
         $field->deleted = TRUE;
       }
     }
-
-    // Clear the cache.
-    \Drupal::entityManager()->clearCachedFieldDefinitions();
   }
 
   /**
diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php
index 62752ae..6e9fca5 100644
--- a/core/modules/system/src/Controller/DbUpdateController.php
+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -285,6 +285,23 @@ protected function selection() {
       drupal_set_message($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'), 'warning');
     }
 
+    // If there are entity schema updates, display their summary.
+    if (\Drupal::service('entity.schema.manager')->hasChanges()) {
+      $entity_build = array();
+      $summary = \Drupal::service('entity.schema.manager')->getChangeSummary();
+      foreach ($summary as $entity_type_id => $items) {
+        $entity_update_key = 'entity_type_updates_' . $entity_type_id;
+        $entity_build[$entity_update_key] = array(
+          '#theme' => 'item_list',
+          '#items' => $items,
+          '#title' => $entity_type_id . ' entity type',
+        );
+        $count++;
+      }
+      // Display these above the module updates, since they will be run first.
+      $build['start'] = $entity_build + $build['start'];
+    }
+
     if (empty($count)) {
       drupal_set_message($this->t('No pending updates.'));
       unset($build);
@@ -501,9 +518,17 @@ protected function triggerBatch(Request $request) {
       $this->state->set('system.maintenance_mode', TRUE);
     }
 
-    $start = $this->getModuleUpdates();
+    $operations = array();
+
+    // First of all perform entity schema updates, if needed, so that subsequent
+    // updates work with a correct entity schema.
+    if (\Drupal::service('entity.schema.manager')->hasChanges()) {
+      $operations[] = array('update_entity_schema', array('system', '0 - Update entity schema'));
+    }
+
     // Resolve any update dependencies to determine the actual updates that will
     // be run and the order they will be run in.
+    $start = $this->getModuleUpdates();
     $updates = update_resolve_dependencies($start);
 
     // Store the dependencies for each update function in an array which the
@@ -515,7 +540,7 @@ protected function triggerBatch(Request $request) {
       $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array();
     }
 
-    $operations = array();
+    // Determine updates to be performed.
     foreach ($updates as $update) {
       if ($update['allowed']) {
         // Set the installed version of each module so updates will start at the
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 8e7b534..8b5e297 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -452,6 +452,11 @@ function system_requirements($phase) {
         }
       }
     }
+    if (!isset($requirements['update']['severity']) && \Drupal::service('entity.schema.manager')->hasChanges()) {
+      $requirements['update']['severity'] = REQUIREMENT_ERROR;
+      $requirements['update']['value'] = t('Out of date');
+      $requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the <a href="@update">database update script</a> immediately.', array('@update' => base_path() . 'update.php'));
+    }
   }
 
   // Verify the update.php access setting
