diff --git a/core/core.services.yml b/core/core.services.yml
index 1c02e0b..459503e 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -510,12 +510,6 @@ services:
       - { name: module_install.uninstall_validator }
     arguments: ['@entity.manager', '@string_translation']
     lazy: true
-  field_uninstall_validator:
-    class: Drupal\Core\Field\FieldModuleUninstallValidator
-    tags:
-      - { name: module_install.uninstall_validator }
-    arguments: ['@entity.manager', '@string_translation']
-    lazy: true
   required_module_uninstall_validator:
     class: Drupal\Core\Extension\RequiredModuleUninstallValidator
     tags:
@@ -576,9 +570,12 @@ services:
   entity.last_installed_schema.repository:
     class: Drupal\Core\Entity\EntityLastInstalledSchemaRepository
     arguments: ['@keyvalue']
+  entity_field.deleted_fields_repository:
+    class: Drupal\Core\Field\DeletedFieldsRepository
+    arguments: ['@state']
   field_storage_definition.listener:
     class: Drupal\Core\Field\FieldStorageDefinitionListener
-    arguments: ['@entity_type.manager', '@event_dispatcher', '@entity.last_installed_schema.repository', '@entity_field.manager']
+    arguments: ['@entity_type.manager', '@event_dispatcher', '@entity.last_installed_schema.repository', '@entity_field.manager', '@entity_field.deleted_fields_repository']
   field_definition.listener:
     class: Drupal\Core\Field\FieldDefinitionListener
     arguments: ['@entity_type.manager', '@entity_field.manager', '@keyvalue', '@cache.discovery']
diff --git a/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php
index 04d1b1e..cf303a2 100644
--- a/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php
@@ -2,9 +2,7 @@
 
 namespace Drupal\Core\Entity;
 
-use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldDefinitionListenerInterface;
-use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
 
 /**
@@ -18,27 +16,4 @@
  */
 interface DynamicallyFieldableEntityStorageInterface extends FieldableEntityStorageInterface, FieldStorageDefinitionListenerInterface, FieldDefinitionListenerInterface {
 
-  /**
-   * Purges a batch of field data.
-   *
-   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
-   *   The deleted field whose data is being purged.
-   * @param $batch_size
-   *   The maximum number of field data records to purge before returning,
-   *   relating to the count of field data records returned by
-   *   \Drupal\Core\Entity\FieldableEntityStorageInterface::countFieldData().
-   *
-   * @return int
-   *   The number of field data records that have been purged.
-   */
-  public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size);
-
-  /**
-   * Performs final cleanup after all data of a field has been purged.
-   *
-   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
-   *   The field being purged.
-   */
-  public function finalizePurge(FieldStorageDefinitionInterface $storage_definition);
-
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
index 70dec2c..d9a4d84 100644
--- a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php
@@ -311,7 +311,8 @@ protected function getChangeList() {
     }
 
     // @todo Support deleting entity definitions when we support base field
-    //   purging. See https://www.drupal.org/node/2282119.
+    //   purging.
+    // @see https://www.drupal.org/node/2907779
 
     $this->entityManager->useCaches(TRUE);
 
diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
index 4d91b29..79c57f9 100644
--- a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php
@@ -2,6 +2,9 @@
 
 namespace Drupal\Core\Entity;
 
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+
 /**
  * A storage that supports entity types with field definitions.
  */
@@ -24,4 +27,27 @@
    */
   public function countFieldData($storage_definition, $as_bool = FALSE);
 
+  /**
+   * Purges a batch of field data.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The deleted field whose data is being purged.
+   * @param int $batch_size
+   *   The maximum number of field data records to purge before returning,
+   *   relating to the count of field data records returned by
+   *   \Drupal\Core\Entity\FieldableEntityStorageInterface::countFieldData().
+   *
+   * @return int
+   *   The number of field data records that have been purged.
+   */
+  public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size);
+
+  /**
+   * Performs final cleanup after all data of a field has been purged.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage being purged.
+   */
+  public function finalizePurge(FieldStorageDefinitionInterface $storage_definition);
+
 }
diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
index fdc7289..efd1ef9 100644
--- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
+++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
@@ -272,7 +272,7 @@ public function setExtraColumns($table_name, array $column_names) {
    *   TRUE if the field can be stored in a dedicated table, FALSE otherwise.
    */
   public function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
-    return !$storage_definition->hasCustomStorage() && $storage_definition->isBaseField() && !$storage_definition->isMultiple();
+    return !$storage_definition->hasCustomStorage() && $storage_definition->isBaseField() && !$storage_definition->isMultiple() && !$storage_definition->isDeleted();
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index 22238d4..910e870 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -19,7 +19,6 @@
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\field\FieldStorageConfigInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -1468,9 +1467,7 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $
       $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
     );
 
-    // @todo Remove the FieldStorageConfigInterface check when non-configurable
-    //   fields support purging: https://www.drupal.org/node/2282119.
-    if ($storage_definition instanceof FieldStorageConfigInterface && $table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+    if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
       // Mark all data associated with the field for deletion.
       $table = $table_mapping->getDedicatedDataTableName($storage_definition);
       $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
@@ -1554,9 +1551,8 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit
     // Check whether the whole field storage definition is gone, or just some
     // bundle fields.
     $storage_definition = $field_definition->getFieldStorageDefinition();
-    $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
     $table_mapping = $this->getTableMapping();
-    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
+    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
 
     // Get the entities which we want to purge first.
     $entity_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]);
@@ -1614,7 +1610,7 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit
    */
   protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
     $storage_definition = $field_definition->getFieldStorageDefinition();
-    $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
+    $is_deleted = $storage_definition->isDeleted();
     $table_mapping = $this->getTableMapping();
     $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
     $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
@@ -1651,7 +1647,7 @@ public function countFieldData($storage_definition, $as_bool = FALSE) {
     $table_mapping = $this->getTableMapping($storage_definitions);
 
     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
-      $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
+      $is_deleted = $storage_definition->isDeleted();
       if ($this->entityType->isRevisionable()) {
         $table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
       }
@@ -1724,15 +1720,12 @@ public function countFieldData($storage_definition, $as_bool = FALSE) {
    *
    * @return bool
    *   Whether the field has been already deleted.
+   *
+   * @deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use
+   *   \Drupal\Core\Field\FieldStorageDefinitionInterface::isDeleted() instead.
    */
   protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
-    // Configurable fields are marked for deletion.
-    if ($storage_definition instanceof FieldStorageConfigInterface) {
-      return $storage_definition->isDeleted();
-    }
-    // For non configurable fields check whether they are still in the last
-    // installed schema repository.
-    return !array_key_exists($storage_definition->getName(), $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityTypeId));
+    return $storage_definition->isDeleted();
   }
 
 }
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
index d180381..a2c94fd 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -3,7 +3,7 @@
 namespace Drupal\Core\Entity\Sql;
 
 use Drupal\Core\Database\Connection;
-use Drupal\Core\Database\DatabaseException;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
@@ -15,7 +15,7 @@
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Field\FieldException;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
-use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Language\LanguageInterface;
 
 /**
  * Defines a schema handler that supports revisionable, translatable entities.
@@ -87,6 +87,13 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
   protected $installedStorageSchema;
 
   /**
+   * The deleted fields repository.
+   *
+   * @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface
+   */
+  protected $deletedFieldsRepository;
+
+  /**
    * Constructs a SqlContentEntityStorageSchema.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
@@ -123,6 +130,23 @@ protected function installedStorageSchema() {
   }
 
   /**
+   * Gets the deleted fields repository.
+   *
+   * @return \Drupal\Core\Field\DeletedFieldsRepositoryInterface
+   *   The deleted fields repository.
+   *
+   * @todo Inject this dependency in the constructor once this class can be
+   *   instantiated as a regular entity handler:
+   *   https://www.drupal.org/node/2332857.
+   */
+  protected function deletedFieldsRepository() {
+    if (!isset($this->deletedFieldsRepository)) {
+      $this->deletedFieldsRepository = \Drupal::service('entity_field.deleted_fields_repository');
+    }
+    return $this->deletedFieldsRepository;
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
@@ -410,25 +434,17 @@ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $
   public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
     // Store original definitions so that switching between shared and dedicated
     // field table layout works.
-    $this->originalDefinitions = $this->fieldStorageDefinitions;
-    $this->originalDefinitions[$original->getName()] = $original;
     $this->performFieldSchemaOperation('update', $storage_definition, $original);
-    $this->originalDefinitions = NULL;
   }
 
   /**
    * {@inheritdoc}
    */
   public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
-    // Only configurable fields currently support purging, so prevent deletion
-    // of ones we can't purge if they have existing data.
-    // @todo Add purging to all fields: https://www.drupal.org/node/2282119.
     try {
-      if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
-        throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data that cannot be purged.');
-      }
+      $has_data = $this->storage->countFieldData($storage_definition, TRUE);
     }
-    catch (DatabaseException $e) {
+    catch (DatabaseExceptionWrapper $e) {
       // This may happen when changing field storage schema, since we are not
       // able to use a table mapping matching the passed storage definition.
       // @todo Revisit this once we are able to instantiate the table mapping
@@ -436,10 +452,22 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $
       return;
     }
 
+    // If the field storage does not have any data, we can safely delete its
+    // schema.
+    if (!$has_data) {
+      $this->performFieldSchemaOperation('delete', $storage_definition);
+      return;
+    }
+
+    // There's nothing else we can do if the field storage has a custom storage.
+    if ($storage_definition->hasCustomStorage()) {
+      return;
+    }
+
     // Retrieve a table mapping which contains the deleted field still.
-    $table_mapping = $this->storage->getTableMapping(
-      $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
-    );
+    $storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id());
+    $table_mapping = $this->storage->getTableMapping($storage_definitions);
+
     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
       // Move the table to a unique name while the table contents are being
       // deleted.
@@ -452,11 +480,72 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $
         $this->database->schema()->renameTable($revision_table, $revision_new_table);
       }
     }
+    else {
+      // Move the field data from the shared table to a dedicated one in order
+      // to allow it to be purged like any other field.
+      $shared_table_field_columns = $table_mapping->getColumnNames($storage_definition->getName());
 
-    // @todo Remove when finalizePurge() is invoked from the outside for all
-    //   fields: https://www.drupal.org/node/2282119.
-    if (!($storage_definition instanceof FieldStorageConfigInterface)) {
-      $this->performFieldSchemaOperation('delete', $storage_definition);
+      // Refresh the table mapping to use the deleted storage definition.
+      $deleted_storage_definition = $this->deletedFieldsRepository()->getFieldStorageDefinitions()[$storage_definition->getUniqueStorageIdentifier()];
+      $original_storage_definitions = [$storage_definition->getName() => $deleted_storage_definition] + $storage_definitions;
+      $table_mapping = $this->storage->getTableMapping($original_storage_definitions);
+
+      $dedicated_table_field_schema = $this->getDedicatedTableSchema($deleted_storage_definition);
+      $dedicated_table_field_columns = $table_mapping->getColumnNames($deleted_storage_definition->getName());
+
+      $dedicated_table_name = $table_mapping->getDedicatedDataTableName($deleted_storage_definition, TRUE);
+      $dedicated_table_name_mapping[$table_mapping->getDedicatedDataTableName($deleted_storage_definition)] = $dedicated_table_name;
+      if ($this->entityType->isRevisionable()) {
+        $dedicated_revision_table_name = $table_mapping->getDedicatedRevisionTableName($deleted_storage_definition, TRUE);
+        $dedicated_table_name_mapping[$table_mapping->getDedicatedRevisionTableName($deleted_storage_definition)] = $dedicated_revision_table_name;
+      }
+
+      // Create the dedicated field tables using "deleted" table names.
+      foreach ($dedicated_table_field_schema as $name => $table) {
+        if (!$this->database->schema()->tableExists($dedicated_table_name_mapping[$name])) {
+          $this->database->schema()->createTable($dedicated_table_name_mapping[$name], $table);
+        }
+        else {
+          throw new EntityStorageException('The field ' . $storage_definition->getName() . ' has already been deleted and it is in the process of being purged.');
+        }
+      }
+
+      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 {
+        // Copy the data from the base table.
+        $is_translatable = $this->entityType->isTranslatable() && $storage_definition->isTranslatable();
+        $base_table = $is_translatable ? $this->storage->getDataTable() : $this->storage->getBaseTable();
+        $this->database->insert($dedicated_table_name)
+          ->from($this->getSelectQueryForFieldStorageDeletion($base_table, $shared_table_field_columns, $dedicated_table_field_columns))
+          ->execute();
+
+        // Copy the data from the revision table.
+        if (isset($dedicated_revision_table_name)) {
+          $revision_table = $is_translatable ? $this->storage->getRevisionDataTable() : $this->storage->getRevisionTable();
+          $this->database->insert($dedicated_revision_table_name)
+            ->from($this->getSelectQueryForFieldStorageDeletion($revision_table, $shared_table_field_columns, $dedicated_table_field_columns, $base_table))
+            ->execute();
+        }
+      }
+      catch (\Exception $e) {
+        if (isset($transaction)) {
+          $transaction->rollBack();
+        }
+        else {
+          // Delete the dedicated tables.
+          foreach ($dedicated_table_field_schema as $name => $table) {
+            $this->database->schema()->dropTable($dedicated_table_name_mapping[$name]);
+          }
+        }
+        throw $e;
+      }
+
+      // Delete the field from the shared tables.
+      $this->deleteSharedTableSchema($storage_definition);
     }
   }
 
@@ -468,6 +557,77 @@ public function finalizePurge(FieldStorageDefinitionInterface $storage_definitio
   }
 
   /**
+   * Returns a SELECT query suitable for inserting data into a dedicated table.
+   *
+   * @param string $table_name
+   *   The entity table name to select from.
+   * @param array $shared_table_field_columns
+   *   An array of field column names for a shared table schema.
+   * @param array $dedicated_table_field_columns
+   *   An array of field column names for a dedicated table schema.
+   * @param string $base_table
+   *   (optional) The name of the base entity table. Defaults to NULL.
+   *
+   * @return \Drupal\Core\Database\Query\SelectInterface
+   *   A database select query.
+   */
+  protected function getSelectQueryForFieldStorageDeletion($table_name, array $shared_table_field_columns, array $dedicated_table_field_columns, $base_table = NULL) {
+    // Create a SELECT query that generates a result suitable for writing into
+    // a dedicated field table.
+    $select = $this->database->select($table_name, 'entity_table');
+
+    // Add the bundle column.
+    if ($bundle = $this->entityType->getKey('bundle')) {
+      if ($base_table) {
+        $select->join($base_table, 'base_table', "entity_table.{$this->entityType->getKey('id')} = %alias.{$this->entityType->getKey('id')}");
+        $select->addField('base_table', $bundle, 'bundle');
+      }
+      else {
+        $select->addField('entity_table', $bundle, 'bundle');
+      }
+    }
+    else {
+      $select->addExpression(':bundle', 'bundle', [':bundle' => $this->entityType->id()]);
+    }
+
+    // Add the deleted column.
+    $select->addExpression(':deleted', 'deleted', [':deleted' => 1]);
+
+    // Add the entity_id column.
+    $select->addField('entity_table', $this->entityType->getKey('id'), 'entity_id');
+
+    // Add the revision_id column.
+    if ($this->entityType->isRevisionable()) {
+      $select->addField('entity_table', $this->entityType->getKey('revision'), 'revision_id');
+    }
+    else {
+      $select->addField('entity_table', $this->entityType->getKey('id'), 'revision_id');
+    }
+
+    // Add the langcode column.
+    if ($langcode = $this->entityType->getKey('langcode')) {
+      $select->addField('entity_table', $langcode, 'langcode');
+    }
+    else {
+      $select->addExpression(':langcode', 'langcode', [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED]);
+    }
+
+    // Add the delta column and set it to 0 because we are only dealing with
+    // single cardinality fields.
+    $select->addExpression(':delta', 'delta', [':delta' => 0]);
+
+    // Add all the dynamic field columns.
+    foreach ($shared_table_field_columns as $field_column_name => $schema_column_name) {
+      $select->addField('entity_table', $schema_column_name, $dedicated_table_field_columns[$field_column_name]);
+    }
+
+    // Lock the table rows.
+    $select->forUpdate(TRUE);
+
+    return $select;
+  }
+
+  /**
    * Checks that we are dealing with the correct entity type.
    *
    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
@@ -1231,17 +1391,13 @@ protected function createSharedTableSchema(FieldStorageDefinitionInterface $stor
    *   The storage definition of the field being deleted.
    */
   protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
-    // When switching from dedicated to shared field table layout we need need
-    // to delete the field tables with their regular names. When this happens
-    // original definitions will be defined.
-    $deleted = !$this->originalDefinitions;
     $table_mapping = $this->storage->getTableMapping();
-    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $deleted);
+    $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
     if ($this->database->schema()->tableExists($table_name)) {
       $this->database->schema()->dropTable($table_name);
     }
     if ($this->entityType->isRevisionable()) {
-      $revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $deleted);
+      $revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $storage_definition->isDeleted());
       if ($this->database->schema()->tableExists($revision_table_name)) {
         $this->database->schema()->dropTable($revision_table_name);
       }
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index 10fabd4..898eb3c 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -408,16 +408,11 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
           $update_manager->uninstallEntityType($entity_type);
         }
         elseif ($entity_type->entityClassImplements(FieldableEntityInterface::CLASS)) {
-          // The module being installed may be adding new fields to existing
-          // entity types. Field definitions for any entity type defined by
-          // the module are handled in the if branch.
-          $entity_type_id = $entity_type->id();
-          /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
-          $storage = $entity_manager->getStorage($entity_type_id);
-          foreach ($entity_manager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) {
-            // @todo We need to trigger field purging here.
-            //   See https://www.drupal.org/node/2282119.
-            if ($storage_definition->getProvider() == $module && !$storage->countFieldData($storage_definition, TRUE)) {
+          // The module being uninstalled might have added new fields to
+          // existing entity types. This will add them to the deleted fields
+          // repository so their data will be purged on cron.
+          foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
+            if ($storage_definition->getProvider() == $module) {
               $update_manager->uninstallFieldStorageDefinition($storage_definition);
             }
           }
diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
index 991385f..4dbf61e 100644
--- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
+++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
@@ -804,6 +804,39 @@ public function getUniqueStorageIdentifier() {
   /**
    * {@inheritdoc}
    */
+  public function getUniqueIdentifier() {
+    // If we have a specified target bundle, we're dealing with a bundle base
+    // field definition, so we need to include it in the unique identifier.
+    if ($this->getTargetBundle()) {
+      return $this->getTargetEntityTypeId() . '-' . $this->getTargetBundle() . '-' . $this->getName();
+    }
+
+    return $this->getUniqueStorageIdentifier();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isDeleted() {
+    return !empty($this->definition['deleted']);
+  }
+
+  /**
+   * Sets whether the field storage is deleted.
+   *
+   * @param bool $deleted
+   *   Whether the field storage is deleted.
+   *
+   * @return $this
+   */
+  public function setDeleted($deleted) {
+    $this->definition['deleted'] = $deleted;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getConfig($bundle) {
     $override = BaseFieldOverride::loadByName($this->getTargetEntityTypeId(), $bundle, $this->getName());
     if ($override) {
diff --git a/core/lib/Drupal/Core/Field/DeletedFieldsRepository.php b/core/lib/Drupal/Core/Field/DeletedFieldsRepository.php
new file mode 100644
index 0000000..59744b6
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/DeletedFieldsRepository.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Drupal\Core\Field;
+
+use Drupal\Core\State\StateInterface;
+
+/**
+ * Provides a repository for deleted field and field storage objects.
+ */
+class DeletedFieldsRepository implements DeletedFieldsRepositoryInterface {
+
+  /**
+   * The state key/value store.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * Constructs a new deleted fields repository.
+   *
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state key/value store.
+   */
+  public function __construct(StateInterface $state) {
+    $this->state = $state;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldDefinitions($field_storage_unique_id = NULL) {
+    $deleted_field_definitions = $this->state->get('field.field.deleted', []);
+
+    if ($field_storage_unique_id) {
+      $deleted_field_definitions = array_filter($deleted_field_definitions, function (FieldDefinitionInterface $field_definition) use ($field_storage_unique_id) {
+        return $field_definition->getFieldStorageDefinition()->getUniqueStorageIdentifier() === $field_storage_unique_id;
+      });
+    }
+
+    return $deleted_field_definitions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldStorageDefinitions() {
+    return $this->state->get('field.storage.deleted', []);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addFieldDefinition(FieldDefinitionInterface $field_definition) {
+    $deleted_field_definitions = $this->state->get('field.field.deleted', []);
+    $deleted_field_definitions[$field_definition->getUniqueIdentifier()] = $field_definition;
+    $this->state->set('field.field.deleted', $deleted_field_definitions);
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addFieldStorageDefinition(FieldStorageDefinitionInterface $field_storage_definition) {
+    $deleted_storage_definitions = $this->state->get('field.storage.deleted', []);
+    $deleted_storage_definitions[$field_storage_definition->getUniqueStorageIdentifier()] = $field_storage_definition;
+    $this->state->set('field.storage.deleted', $deleted_storage_definitions);
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeFieldDefinition(FieldDefinitionInterface $field_definition) {
+    $deleted_field_definitions = $this->state->get('field.field.deleted', []);;
+    unset($deleted_field_definitions[$field_definition->getUniqueIdentifier()]);
+    $this->state->set('field.field.deleted', $deleted_field_definitions);
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeFieldStorageDefinition(FieldStorageDefinitionInterface $field_storage_definition) {
+    $deleted_storage_definitions = $this->state->get('field.storage.deleted', []);
+    unset($deleted_storage_definitions[$field_storage_definition->getUniqueStorageIdentifier()]);
+    $this->state->set('field.storage.deleted', $deleted_storage_definitions);
+
+    return $this;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Field/DeletedFieldsRepositoryInterface.php b/core/lib/Drupal/Core/Field/DeletedFieldsRepositoryInterface.php
new file mode 100644
index 0000000..eabbd10
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/DeletedFieldsRepositoryInterface.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\Core\Field;
+
+/**
+ * Provides an interface for a deleted fields repository.
+ */
+interface DeletedFieldsRepositoryInterface {
+
+  /**
+   * Returns a list of deleted field definitions.
+   *
+   * @param string $field_storage_unique_id
+   *   (optional) A unique ID of field storage definition for filtering the
+   *   deleted fields. Defaults to NULL.
+   *
+   * @return \Drupal\Core\Field\FieldDefinitionInterface[]
+   *   An array of field definition objects, keyed by their unique identifier.
+   */
+  public function getFieldDefinitions($field_storage_unique_id = NULL);
+
+  /**
+   * Returns a list of deleted field storage definitions.
+   *
+   * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
+   *   An array of field storage definition objects, keyed by their unique
+   *   storage identifier.
+   */
+  public function getFieldStorageDefinitions();
+
+  /**
+   * Adds a field definition object to the deleted list.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   A field definition object.
+   *
+   * @return $this
+   */
+  public function addFieldDefinition(FieldDefinitionInterface $field_definition);
+
+  /**
+   * Adds a field storage definition object to the deleted list.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition
+   *   A field storage definition object.
+   *
+   * @return $this
+   */
+  public function addFieldStorageDefinition(FieldStorageDefinitionInterface $field_storage_definition);
+
+  /**
+   * Removes a field definition object from the deleted list.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   A field definition object.
+   *
+   * @return $this
+   */
+  public function removeFieldDefinition(FieldDefinitionInterface $field_definition);
+
+  /**
+   * Removes a field storage definition object from the deleted list.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition
+   *   A field storage definition object.
+   *
+   * @return $this
+   */
+  public function removeFieldStorageDefinition(FieldStorageDefinitionInterface $field_storage_definition);
+
+}
diff --git a/core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php b/core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php
index b31833a..555e22a 100644
--- a/core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php
+++ b/core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php
@@ -150,6 +150,13 @@ public function getClass() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function getUniqueIdentifier() {
+    return $this->getBaseFieldDefinition()->getUniqueIdentifier();
+  }
+
+  /**
    * Gets the base field definition.
    *
    * @return \Drupal\Core\Field\BaseFieldDefinition
diff --git a/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php b/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php
index dbfda44..c99a11a 100644
--- a/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php
+++ b/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php
@@ -259,4 +259,11 @@ public function getFieldStorageDefinition();
    */
   public function getConfig($bundle);
 
+  /**
+   * Returns a unique identifier for the field.
+   *
+   * @return string
+   */
+  public function getUniqueIdentifier();
+
 }
diff --git a/core/lib/Drupal/Core/Field/FieldModuleUninstallValidator.php b/core/lib/Drupal/Core/Field/FieldModuleUninstallValidator.php
deleted file mode 100644
index 08a27cb..0000000
--- a/core/lib/Drupal/Core/Field/FieldModuleUninstallValidator.php
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-namespace Drupal\Core\Field;
-
-use Drupal\Core\Entity\EntityManagerInterface;
-use Drupal\Core\Entity\FieldableEntityInterface;
-use Drupal\Core\Entity\FieldableEntityStorageInterface;
-use Drupal\Core\Extension\ModuleUninstallValidatorInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\StringTranslation\TranslationInterface;
-
-/**
- * Validates module uninstall readiness based on defined storage definitions.
- *
- * @todo Remove this once we support field purging for base fields. See
- *   https://www.drupal.org/node/2282119.
- */
-class FieldModuleUninstallValidator implements ModuleUninstallValidatorInterface {
-  use StringTranslationTrait;
-
-  /**
-   * Constructs the object.
-   *
-   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
-   *   The entity manager.
-   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
-   *   The string translation service.
-   */
-  public function __construct(EntityManagerInterface $entity_manager, TranslationInterface $string_translation) {
-    $this->entityManager = $entity_manager;
-    $this->stringTranslation = $string_translation;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validate($module_name) {
-    $reasons = [];
-
-    // We skip fields provided by the Field module as it implements field
-    // purging.
-    if ($module_name != 'field') {
-      foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
-        // We skip entity types defined by the module as there must be no
-        // content to be able to uninstall them anyway.
-        // See \Drupal\Core\Entity\ContentUninstallValidator.
-        if ($entity_type->getProvider() != $module_name && $entity_type->entityClassImplements(FieldableEntityInterface::class)) {
-          foreach ($this->entityManager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) {
-            if ($storage_definition->getProvider() == $module_name) {
-              $storage = $this->entityManager->getStorage($entity_type_id);
-              if ($storage instanceof FieldableEntityStorageInterface && $storage->countFieldData($storage_definition, TRUE)) {
-                $reasons[] = $this->t('There is data for the field @field-name on entity type @entity_type', [
-                  '@field-name' => $storage_definition->getName(),
-                  '@entity_type' => $entity_type->getLabel(),
-                ]);
-              }
-            }
-          }
-        }
-      }
-    }
-
-    return $reasons;
-  }
-
-}
diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php
index d703659..9331d94 100644
--- a/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php
+++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php
@@ -333,10 +333,18 @@ public function hasCustomStorage();
   public function isBaseField();
 
   /**
-   * Returns a unique identifier for the field.
+   * Returns a unique identifier for the field storage.
    *
    * @return string
    */
   public function getUniqueStorageIdentifier();
 
+  /**
+   * Returns whether the field is deleted or not.
+   *
+   * @return bool
+   *   TRUE if the field is deleted, FALSE otherwise.
+   */
+  public function isDeleted();
+
 }
diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php
index 154e485..e67629b 100644
--- a/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php
+++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php
@@ -2,9 +2,11 @@
 
 namespace Drupal\Core\Field;
 
+use Drupal\Core\Database\DatabaseExceptionWrapper;
 use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\FieldableEntityStorageInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
@@ -44,6 +46,13 @@ class FieldStorageDefinitionListener implements FieldStorageDefinitionListenerIn
   protected $entityFieldManager;
 
   /**
+   * The deleted fields repository.
+   *
+   * @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface
+   */
+  protected $deletedFieldsRepository;
+
+  /**
    * Constructs a new FieldStorageDefinitionListener.
    *
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
@@ -54,12 +63,15 @@ class FieldStorageDefinitionListener implements FieldStorageDefinitionListenerIn
    *   The entity last installed schema repository.
    * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
    *   The entity field manager.
+   * @param \Drupal\Core\Field\DeletedFieldsRepositoryInterface $deleted_fields_repository
+   *   The deleted fields repository.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher, EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository, EntityFieldManagerInterface $entity_field_manager) {
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher, EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository, EntityFieldManagerInterface $entity_field_manager, DeletedFieldsRepositoryInterface $deleted_fields_repository) {
     $this->entityTypeManager = $entity_type_manager;
     $this->eventDispatcher = $event_dispatcher;
     $this->entityLastInstalledSchemaRepository = $entity_last_installed_schema_repository;
     $this->entityFieldManager = $entity_field_manager;
+    $this->deletedFieldsRepository = $deleted_fields_repository;
   }
 
   /**
@@ -139,6 +151,23 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $
       $storage->setEntityType($last_installed_entity_type);
     }
 
+    // Keep the field definition in the deleted fields repository so we can use
+    // it later during field_purge_batch(), but only if the field has data.
+    try {
+      if ($storage_definition instanceof BaseFieldDefinition && $storage instanceof FieldableEntityStorageInterface && $storage->countFieldData($storage_definition, TRUE)) {
+        $deleted_storage_definition = clone $storage_definition;
+        $deleted_storage_definition->setDeleted(TRUE);
+        $this->deletedFieldsRepository->addFieldDefinition($deleted_storage_definition);
+        $this->deletedFieldsRepository->addFieldStorageDefinition($deleted_storage_definition);
+      }
+    }
+    catch (DatabaseExceptionWrapper $e) {
+      // This may happen when changing field storage schema, since we are not
+      // able to use a table mapping matching the passed storage definition.
+      // @todo Revisit this once we are able to instantiate the table mapping
+      //   properly. See https://www.drupal.org/node/2274017.
+    }
+
     if ($storage instanceof FieldStorageDefinitionListenerInterface) {
       $storage->onFieldStorageDefinitionDelete($storage_definition);
     }
diff --git a/core/lib/Drupal/Core/ProxyClass/Field/FieldModuleUninstallValidator.php b/core/lib/Drupal/Core/ProxyClass/Field/FieldModuleUninstallValidator.php
deleted file mode 100644
index f15745f..0000000
--- a/core/lib/Drupal/Core/ProxyClass/Field/FieldModuleUninstallValidator.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-// @codingStandardsIgnoreFile
-
-/**
- * This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\Core\Field\FieldModuleUninstallValidator' "core/lib/Drupal/Core".
- */
-
-namespace Drupal\Core\ProxyClass\Field {
-
-    /**
-     * Provides a proxy class for \Drupal\Core\Field\FieldModuleUninstallValidator.
-     *
-     * @see \Drupal\Component\ProxyBuilder
-     */
-    class FieldModuleUninstallValidator implements \Drupal\Core\Extension\ModuleUninstallValidatorInterface
-    {
-
-        use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
-
-        /**
-         * The id of the original proxied service.
-         *
-         * @var string
-         */
-        protected $drupalProxyOriginalServiceId;
-
-        /**
-         * The real proxied service, after it was lazy loaded.
-         *
-         * @var \Drupal\Core\Field\FieldModuleUninstallValidator
-         */
-        protected $service;
-
-        /**
-         * The service container.
-         *
-         * @var \Symfony\Component\DependencyInjection\ContainerInterface
-         */
-        protected $container;
-
-        /**
-         * Constructs a ProxyClass Drupal proxy object.
-         *
-         * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
-         *   The container.
-         * @param string $drupal_proxy_original_service_id
-         *   The service ID of the original service.
-         */
-        public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
-        {
-            $this->container = $container;
-            $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
-        }
-
-        /**
-         * Lazy loads the real service from the container.
-         *
-         * @return object
-         *   Returns the constructed real service.
-         */
-        protected function lazyLoadItself()
-        {
-            if (!isset($this->service)) {
-                $this->service = $this->container->get($this->drupalProxyOriginalServiceId);
-            }
-
-            return $this->service;
-        }
-
-        /**
-         * {@inheritdoc}
-         */
-        public function validate($module_name)
-        {
-            return $this->lazyLoadItself()->validate($module_name);
-        }
-
-        /**
-         * {@inheritdoc}
-         */
-        public function setStringTranslation(\Drupal\Core\StringTranslation\TranslationInterface $translation)
-        {
-            return $this->lazyLoadItself()->setStringTranslation($translation);
-        }
-
-    }
-
-}
diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module
index 8dbd170..6a0c229 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -183,8 +183,8 @@ function content_translation_entity_base_field_info(EntityTypeInterface $entity_
     // when translation is disabled.
     // @todo Re-evaluate this approach and consider removing field storage
     //   definitions and the related field data if the entity type has no bundle
-    //   enabled for translation, once base field purging is supported.
-    //   See https://www.drupal.org/node/2282119.
+    //   enabled for translation.
+    // @see https://www.drupal.org/node/2907777
     if ($manager->isEnabled($entity_type_id) || array_intersect_key($definitions, $installed_storage_definitions)) {
       return $definitions;
     }
diff --git a/core/modules/field/field.purge.inc b/core/modules/field/field.purge.inc
index 59f347a..a2b2b92 100644
--- a/core/modules/field/field.purge.inc
+++ b/core/modules/field/field.purge.inc
@@ -5,10 +5,9 @@
  * Provides support for field data purge after mass deletion.
  */
 
+use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldException;
-use Drupal\field\Entity\FieldStorageConfig;
-use Drupal\field\FieldStorageConfigInterface;
-use Drupal\field\FieldConfigInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * @defgroup field_purge Field API bulk data deletion
@@ -67,20 +66,16 @@
  * be purged. If a deleted field storage with no remaining fields is found, the
  * field storage itself will be purged.
  *
- * @param $batch_size
+ * @param int $batch_size
  *   The maximum number of field data records to purge before returning.
- * @param string $field_storage_uuid
- *   (optional) Limit the purge to a specific field storage.
+ * @param string $field_storage_unique_id
+ *   (optional) Limit the purge to a specific field storage. Defaults to NULL.
  */
-function field_purge_batch($batch_size, $field_storage_uuid = NULL) {
-  $properties = [
-    'deleted' => TRUE,
-    'include_deleted' => TRUE,
-  ];
-  if ($field_storage_uuid) {
-    $properties['field_storage_uuid'] = $field_storage_uuid;
-  }
-  $fields = entity_load_multiple_by_properties('field_config', $properties);
+function field_purge_batch($batch_size, $field_storage_unique_id = NULL) {
+  /** @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface $deleted_fields_repository */
+  $deleted_fields_repository = \Drupal::service('entity_field.deleted_fields_repository');
+
+  $fields = $deleted_fields_repository->getFieldDefinitions($field_storage_unique_id);
 
   $info = \Drupal::entityManager()->getDefinitions();
   foreach ($fields as $field) {
@@ -107,10 +102,8 @@ function field_purge_batch($batch_size, $field_storage_uuid = NULL) {
   }
 
   // Retrieve all deleted field storages. Any that have no fields can be purged.
-  $deleted_storages = \Drupal::state()->get('field.storage.deleted') ?: [];
-  foreach ($deleted_storages as $field_storage) {
-    $field_storage = new FieldStorageConfig($field_storage);
-    if ($field_storage_uuid && $field_storage->uuid() != $field_storage_uuid) {
+  foreach ($deleted_fields_repository->getFieldStorageDefinitions() as $field_storage) {
+    if ($field_storage_unique_id && $field_storage->getUniqueStorageIdentifier() != $field_storage_unique_id) {
       // If a specific UUID is provided, only purge the corresponding field.
       continue;
     }
@@ -122,7 +115,7 @@ function field_purge_batch($batch_size, $field_storage_uuid = NULL) {
       continue;
     }
 
-    $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]);
+    $fields = $deleted_fields_repository->getFieldDefinitions($field_storage->getUniqueStorageIdentifier());
     if (empty($fields)) {
       field_purge_field_storage($field_storage);
     }
@@ -135,14 +128,13 @@ function field_purge_batch($batch_size, $field_storage_uuid = NULL) {
  * This function assumes all data for the field has already been purged and
  * should only be called by field_purge_batch().
  *
- * @param $field
- *   The field record to purge.
+ * @param \Drupal\Core\Field\FieldDefinitionInterface $field
+ *   The field to purge.
  */
-function field_purge_field(FieldConfigInterface $field) {
-  $state = \Drupal::state();
-  $deleted_fields = $state->get('field.field.deleted');
-  unset($deleted_fields[$field->uuid()]);
-  $state->set('field.field.deleted', $deleted_fields);
+function field_purge_field(FieldDefinitionInterface $field) {
+  /** @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface $deleted_fields_repository */
+  $deleted_fields_repository = \Drupal::service('entity_field.deleted_fields_repository');
+  $deleted_fields_repository->removeFieldDefinition($field);
 
   // Invoke external hooks after the cache is cleared for API consistency.
   \Drupal::moduleHandler()->invokeAll('field_purge_field', [$field]);
@@ -154,21 +146,21 @@ function field_purge_field(FieldConfigInterface $field) {
  * This function assumes all fields for the field storage has already been
  * purged, and should only be called by field_purge_batch().
  *
- * @param \Drupal\field\FieldStorageConfigInterface $field_storage
+ * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage
  *   The field storage to purge.
  *
- * @throws Drupal\field\FieldException
+ * @throws \Drupal\Core\Field\FieldException
  */
-function field_purge_field_storage(FieldStorageConfigInterface $field_storage) {
-  $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]);
+function field_purge_field_storage(FieldStorageDefinitionInterface $field_storage) {
+  /** @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface $deleted_fields_repository */
+  $deleted_fields_repository = \Drupal::service('entity_field.deleted_fields_repository');
+
+  $fields = $deleted_fields_repository->getFieldDefinitions($field_storage->getUniqueStorageIdentifier());
   if (count($fields) > 0) {
     throw new FieldException(t('Attempt to purge a field storage @field_name that still has fields.', ['@field_name' => $field_storage->getName()]));
   }
 
-  $state = \Drupal::state();
-  $deleted_storages = $state->get('field.storage.deleted');
-  unset($deleted_storages[$field_storage->uuid()]);
-  $state->set('field.storage.deleted', $deleted_storages);
+  $deleted_fields_repository->removeFieldStorageDefinition($field_storage);
 
   // Notify the storage layer.
   \Drupal::entityManager()->getStorage($field_storage->getTargetEntityTypeId())->finalizePurge($field_storage);
diff --git a/core/modules/field/src/ConfigImporterFieldPurger.php b/core/modules/field/src/ConfigImporterFieldPurger.php
index 3974511..c38fc4b 100644
--- a/core/modules/field/src/ConfigImporterFieldPurger.php
+++ b/core/modules/field/src/ConfigImporterFieldPurger.php
@@ -39,7 +39,7 @@ public static function process(array &$context, ConfigImporter $config_importer)
         $field_storage->delete();
       }
     }
-    field_purge_batch($context['sandbox']['field']['purge_batch_size'], $field_storage->uuid());
+    field_purge_batch($context['sandbox']['field']['purge_batch_size'], $field_storage->getUniqueStorageIdentifier());
     $context['sandbox']['field']['current_progress']++;
     $fields_to_delete_count = count(static::getFieldStoragesToPurge($context['sandbox']['field']['extensions'], $config_importer->getUnprocessedConfiguration('delete')));
     if ($fields_to_delete_count == 0) {
@@ -133,11 +133,11 @@ public static function getFieldStoragesToPurge(array $extensions, array $deletes
     }
 
     // Gather deleted fields from modules that are being uninstalled.
-    /** @var \Drupal\field\FieldStorageConfigInterface[] $field_storages */
-    $field_storages = entity_load_multiple_by_properties('field_storage_config', ['deleted' => TRUE, 'include_deleted' => TRUE]);
-    foreach ($field_storages as $field_storage) {
-      if (!in_array($field_storage->getTypeProvider(), $providers)) {
-        $storages_to_delete[$field_storage->id()] = $field_storage;
+    /** @var \Drupal\field\FieldStorageConfigInterface[] $deleted_storage_definitions */
+    $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
+    foreach ($deleted_storage_definitions as $field_storage_definition) {
+      if ($field_storage_definition instanceof FieldStorageConfigInterface && !in_array($field_storage_definition->getTypeProvider(), $providers)) {
+        $storages_to_delete[$field_storage_definition->id()] = $field_storage_definition;
       }
     }
     return $storages_to_delete;
diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php
index 49b9141..af17eee 100644
--- a/core/modules/field/src/Entity/FieldConfig.php
+++ b/core/modules/field/src/Entity/FieldConfig.php
@@ -189,27 +189,26 @@ public function calculateDependencies() {
    * {@inheritdoc}
    */
   public static function preDelete(EntityStorageInterface $storage, array $fields) {
-    $state = \Drupal::state();
+    /** @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface $deleted_fields_repository */
+    $deleted_fields_repository = \Drupal::service('entity_field.deleted_fields_repository');
     $entity_type_manager = \Drupal::entityTypeManager();
 
     parent::preDelete($storage, $fields);
-    // Keep the field definitions in the state storage so we can use them
-    // later during field_purge_batch().
-    $deleted_fields = $state->get('field.field.deleted') ?: [];
 
+    // Keep the field definitions in the deleted fields repository so we can use
+    // them later during field_purge_batch().
     /** @var \Drupal\field\FieldConfigInterface $field */
     foreach ($fields as $field) {
       // Only mark a field for purging if there is data. Otherwise, just remove
       // it.
       $target_entity_storage = $entity_type_manager->getStorage($field->getTargetEntityTypeId());
       if (!$field->deleted && $target_entity_storage instanceof FieldableEntityStorageInterface && $target_entity_storage->countFieldData($field->getFieldStorageDefinition(), TRUE)) {
-        $config = $field->toArray();
-        $config['deleted'] = TRUE;
-        $config['field_storage_uuid'] = $field->getFieldStorageDefinition()->uuid();
-        $deleted_fields[$field->uuid()] = $config;
+        $field = clone $field;
+        $field->deleted = TRUE;
+        $field->fieldStorage = NULL;
+        $deleted_fields_repository->addFieldDefinition($field);
       }
     }
-    $state->set('field.field.deleted', $deleted_fields);
   }
 
   /**
@@ -288,14 +287,30 @@ public function isDeleted() {
    */
   public function getFieldStorageDefinition() {
     if (!$this->fieldStorage) {
-      $fields = $this->entityManager()->getFieldStorageDefinitions($this->entity_type);
-      if (!isset($fields[$this->field_name])) {
+      $field_storage_definition = NULL;
+
+      $field_storage_definitions = $this->entityManager()->getFieldStorageDefinitions($this->entity_type);
+      if (isset($field_storage_definitions[$this->field_name])) {
+        $field_storage_definition = $field_storage_definitions[$this->field_name];
+      }
+      // If this field has been deleted, try to find its field storage
+      // definition in the deleted fields repository.
+      elseif ($this->deleted) {
+        $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
+        foreach ($deleted_storage_definitions as $deleted_storage_definition) {
+          if ($deleted_storage_definition->getName() === $this->field_name) {
+            $field_storage_definition = $deleted_storage_definition;
+          }
+        }
+      }
+
+      if (!$field_storage_definition) {
         throw new FieldException("Attempt to create a field {$this->field_name} that does not exist on entity type {$this->entity_type}.");
       }
-      if (!$fields[$this->field_name] instanceof FieldStorageConfigInterface) {
+      if (!$field_storage_definition instanceof FieldStorageConfigInterface) {
         throw new FieldException("Attempt to create a configurable field of non-configurable field storage {$this->field_name}.");
       }
-      $this->fieldStorage = $fields[$this->field_name];
+      $this->fieldStorage = $field_storage_definition;
     }
 
     return $this->fieldStorage;
@@ -331,6 +346,13 @@ public function isComputed() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function getUniqueIdentifier() {
+    return $this->uuid();
+  }
+
+  /**
    * Loads a field config entity based on the entity type and field name.
    *
    * @param string $entity_type_id
diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php
index bf2641e..6ae439e 100644
--- a/core/modules/field/src/Entity/FieldStorageConfig.php
+++ b/core/modules/field/src/Entity/FieldStorageConfig.php
@@ -398,7 +398,8 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
    * {@inheritdoc}
    */
   public static function preDelete(EntityStorageInterface $storage, array $field_storages) {
-    $state = \Drupal::state();
+    /** @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface $deleted_fields_repository */
+    $deleted_fields_repository = \Drupal::service('entity_field.deleted_fields_repository');
 
     // Set the static flag so that we don't delete field storages whilst
     // deleting fields.
@@ -407,23 +408,19 @@ public static function preDelete(EntityStorageInterface $storage, array $field_s
     // Delete or fix any configuration that is dependent, for example, fields.
     parent::preDelete($storage, $field_storages);
 
-    // Keep the field definitions in the state storage so we can use them later
-    // during field_purge_batch().
-    $deleted_storages = $state->get('field.storage.deleted') ?: [];
+    // Keep the field storage definitions in the deleted fields repository so we
+    // can use them later during field_purge_batch().
     /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
     foreach ($field_storages as $field_storage) {
       // Only mark a field for purging if there is data. Otherwise, just remove
       // it.
       $target_entity_storage = \Drupal::entityTypeManager()->getStorage($field_storage->getTargetEntityTypeId());
       if (!$field_storage->deleted && $target_entity_storage instanceof FieldableEntityStorageInterface && $target_entity_storage->countFieldData($field_storage, TRUE)) {
-        $config = $field_storage->toArray();
-        $config['deleted'] = TRUE;
-        $config['bundles'] = $field_storage->getBundles();
-        $deleted_storages[$field_storage->uuid()] = $config;
+        $storage_definition = clone $field_storage;
+        $storage_definition->deleted = TRUE;
+        $deleted_fields_repository->addFieldStorageDefinition($storage_definition);
       }
     }
-
-    $state->set('field.storage.deleted', $deleted_storages);
   }
 
   /**
diff --git a/core/modules/field/src/FieldConfigStorage.php b/core/modules/field/src/FieldConfigStorage.php
index 42d9634..a0e460f 100644
--- a/core/modules/field/src/FieldConfigStorage.php
+++ b/core/modules/field/src/FieldConfigStorage.php
@@ -5,16 +5,16 @@
 use Drupal\Core\Config\Config;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\DeletedFieldsRepositoryInterface;
 use Drupal\Core\Field\FieldConfigStorageBase;
 use Drupal\Core\Field\FieldTypePluginManagerInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Component\Uuid\UuidInterface;
-use Drupal\Core\State\StateInterface;
 
 /**
- * Controller class for fields.
+ * Storage handler for field config.
  */
 class FieldConfigStorage extends FieldConfigStorageBase {
 
@@ -26,18 +26,18 @@ class FieldConfigStorage extends FieldConfigStorageBase {
   protected $entityManager;
 
   /**
-   * The state keyvalue collection.
+   * The field type plugin manager.
    *
-   * @var \Drupal\Core\State\StateInterface
+   * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
    */
-  protected $state;
+  protected $fieldTypeManager;
 
   /**
-   * The field type plugin manager.
+   * The deleted fields repository.
    *
-   * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
+   * @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface
    */
-  protected $fieldTypeManager;
+  protected $deletedFieldsRepository;
 
   /**
    * Constructs a FieldConfigStorage object.
@@ -52,16 +52,16 @@ class FieldConfigStorage extends FieldConfigStorageBase {
    *   The language manager.
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
    *   The entity manager.
-   * @param \Drupal\Core\State\StateInterface $state
-   *   The state key value store.
    * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
    *   The field type plugin manager.
+   * @param \Drupal\Core\Field\DeletedFieldsRepositoryInterface $deleted_fields_repository
+   *   The deleted fields repository.
    */
-  public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, EntityManagerInterface $entity_manager, StateInterface $state, FieldTypePluginManagerInterface $field_type_manager) {
+  public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager, DeletedFieldsRepositoryInterface $deleted_fields_repository) {
     parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
     $this->entityManager = $entity_manager;
-    $this->state = $state;
     $this->fieldTypeManager = $field_type_manager;
+    $this->deletedFieldsRepository = $deleted_fields_repository;
   }
 
   /**
@@ -74,8 +74,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
       $container->get('uuid'),
       $container->get('language_manager'),
       $container->get('entity.manager'),
-      $container->get('state'),
-      $container->get('plugin.manager.field.field_type')
+      $container->get('plugin.manager.field.field_type'),
+      $container->get('entity_field.deleted_fields_repository')
     );
   }
 
@@ -104,7 +104,7 @@ public function loadByProperties(array $conditions = []) {
 
     // Get fields stored in configuration. If we are explicitly looking for
     // deleted fields only, this can be skipped, because they will be
-    // retrieved from state below.
+    // retrieved from the deleted fields repository below.
     if (empty($conditions['deleted'])) {
       if (isset($conditions['entity_type']) && isset($conditions['bundle']) && isset($conditions['field_name'])) {
         // Optimize for the most frequent case where we do have a specific ID.
@@ -117,16 +117,13 @@ public function loadByProperties(array $conditions = []) {
       }
     }
 
-    // Merge deleted fields (stored in state) if needed.
+    // Merge deleted fields from the deleted fields repository if needed.
     if ($include_deleted || !empty($conditions['deleted'])) {
-      $deleted_fields = $this->state->get('field.field.deleted') ?: [];
-      $deleted_storages = $this->state->get('field.storage.deleted') ?: [];
-      foreach ($deleted_fields as $id => $config) {
-        // If the field storage itself is deleted, inject it directly in the field.
-        if (isset($deleted_storages[$config['field_storage_uuid']])) {
-          $config['field_storage'] = $this->entityManager->getStorage('field_storage_config')->create($deleted_storages[$config['field_storage_uuid']]);
+      $deleted_field_definitions = $this->deletedFieldsRepository->getFieldDefinitions();
+      foreach ($deleted_field_definitions as $id => $field_definition) {
+        if ($field_definition instanceof FieldConfigInterface) {
+          $fields[$id] = $field_definition;
         }
-        $fields[$id] = $this->create($config);
       }
     }
 
diff --git a/core/modules/field/src/FieldStorageConfigInterface.php b/core/modules/field/src/FieldStorageConfigInterface.php
index 1b66621..1f3fed2 100644
--- a/core/modules/field/src/FieldStorageConfigInterface.php
+++ b/core/modules/field/src/FieldStorageConfigInterface.php
@@ -35,14 +35,6 @@ public function getTypeProvider();
   public function getBundles();
 
   /**
-   * Returns whether the field is deleted or not.
-   *
-   * @return bool
-   *   TRUE if the field is deleted.
-   */
-  public function isDeleted();
-
-  /**
    * Checks if the field storage can be deleted.
    *
    * @return bool
diff --git a/core/modules/field/src/FieldStorageConfigStorage.php b/core/modules/field/src/FieldStorageConfigStorage.php
index 7a793f6..3c3d8d8 100644
--- a/core/modules/field/src/FieldStorageConfigStorage.php
+++ b/core/modules/field/src/FieldStorageConfigStorage.php
@@ -7,15 +7,15 @@
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\DeletedFieldsRepositoryInterface;
 use Drupal\Core\Field\FieldTypePluginManagerInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\State\StateInterface;
 
 /**
- * Controller class for "field storage" configuration entities.
+ * Storage handler for "field storage" configuration entities.
  */
 class FieldStorageConfigStorage extends ConfigEntityStorage {
 
@@ -34,18 +34,18 @@ class FieldStorageConfigStorage extends ConfigEntityStorage {
   protected $entityManager;
 
   /**
-   * The state keyvalue collection.
+   * The field type plugin manager.
    *
-   * @var \Drupal\Core\State\StateInterface
+   * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
    */
-  protected $state;
+  protected $fieldTypeManager;
 
   /**
-   * The field type plugin manager.
+   * The deleted fields repository.
    *
-   * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
+   * @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface
    */
-  protected $fieldTypeManager;
+  protected $deletedFieldsRepository;
 
   /**
    * Constructs a FieldStorageConfigStorage object.
@@ -62,17 +62,17 @@ class FieldStorageConfigStorage extends ConfigEntityStorage {
    *   The entity manager.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler.
-   * @param \Drupal\Core\State\StateInterface $state
-   *   The state key value store.
-   * @param \Drupal\Component\Plugin\PluginManagerInterface\FieldTypePluginManagerInterface $field_type_manager
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
    *   The field type plugin manager.
+   * @param \Drupal\Core\Field\DeletedFieldsRepositoryInterface $deleted_fields_repository
+   *   The deleted fields repository.
    */
-  public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, StateInterface $state, FieldTypePluginManagerInterface $field_type_manager) {
+  public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, FieldTypePluginManagerInterface $field_type_manager, DeletedFieldsRepositoryInterface $deleted_fields_repository) {
     parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
     $this->entityManager = $entity_manager;
     $this->moduleHandler = $module_handler;
-    $this->state = $state;
     $this->fieldTypeManager = $field_type_manager;
+    $this->deletedFieldsRepository = $deleted_fields_repository;
   }
 
   /**
@@ -86,8 +86,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
       $container->get('language_manager'),
       $container->get('entity.manager'),
       $container->get('module_handler'),
-      $container->get('state'),
-      $container->get('plugin.manager.field.field_type')
+      $container->get('plugin.manager.field.field_type'),
+      $container->get('entity_field.deleted_fields_repository')
     );
   }
 
@@ -104,7 +104,7 @@ public function loadByProperties(array $conditions = []) {
 
     // Get field storages living in configuration. If we are explicitly looking
     // for deleted storages only, this can be skipped, because they will be
-    // retrieved from state below.
+    // retrieved from the deleted fields repository below.
     if (empty($conditions['deleted'])) {
       if (isset($conditions['entity_type']) && isset($conditions['field_name'])) {
         // Optimize for the most frequent case where we do have a specific ID.
@@ -117,11 +117,14 @@ public function loadByProperties(array $conditions = []) {
       }
     }
 
-    // Merge deleted field storages (living in state) if needed.
+    // Merge deleted field storage definitions from the deleted fields
+    // repository if needed.
     if ($include_deleted || !empty($conditions['deleted'])) {
-      $deleted_storages = $this->state->get('field.storage.deleted') ?: [];
-      foreach ($deleted_storages as $id => $config) {
-        $storages[$id] = $this->create($config);
+      $deleted_storage_definitions = $this->deletedFieldsRepository->getFieldStorageDefinitions();
+      foreach ($deleted_storage_definitions as $id => $field_storage_definition) {
+        if ($field_storage_definition instanceof FieldStorageConfigInterface) {
+          $storages[$id] = $field_storage_definition;
+        }
       }
     }
 
diff --git a/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module
index 45892a8..50bf3e3 100644
--- a/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module
+++ b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module
@@ -94,5 +94,6 @@ function entity_schema_test_entity_bundle_delete($entity_type_id, $bundle) {
       ->setTargetBundle($bundle);
     // Notify the entity storage that our field is gone.
     \Drupal::entityManager()->onFieldDefinitionDelete($field_definitions['custom_bundle_field']);
+    \Drupal::entityManager()->onFieldStorageDefinitionDelete($field_definitions['custom_bundle_field']);
   }
 }
diff --git a/core/modules/system/tests/src/Functional/Entity/Update/UpdateApiEntityDefinitionUpdateTest.php b/core/modules/system/tests/src/Functional/Entity/Update/UpdateApiEntityDefinitionUpdateTest.php
index 89c1d70..f3285ae 100644
--- a/core/modules/system/tests/src/Functional/Entity/Update/UpdateApiEntityDefinitionUpdateTest.php
+++ b/core/modules/system/tests/src/Functional/Entity/Update/UpdateApiEntityDefinitionUpdateTest.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\Tests\system\Functional\Entity\Update;
 
-use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\Tests\system\Functional\Update\DbUpdatesTrait;
@@ -165,30 +164,12 @@ public function testStatusReport() {
     $this->assertNoRaw('Out of date');
     $this->assertRaw('Mismatched entity and/or field definitions');
 
-    // Check that en exception would be triggered when trying to apply them with
-    // existing data.
-    $message = 'Entity updates cannot run if entity data exists.';
-    try {
-      $this->updatesManager->applyUpdates();
-      $this->fail($message);
-    }
-    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
-      $this->pass($message);
-    }
-
-    // Check the status report is the same after trying to apply updates.
+    // Apply the entity updates and check that the entity update status report
+    // item is no longer displayed.
+    $this->updatesManager->applyUpdates();
     $this->drupalGet('admin/reports/status');
     $this->assertNoRaw('Out of date');
-    $this->assertRaw('Mismatched entity and/or field definitions');
-
-    // Delete entity data, enable a new update, run updates again and check that
-    // entity updates were not applied even when no data exists.
-    $entity->delete();
-    $this->enableUpdates('entity_test', 'status_report', 8002);
-    $this->applyUpdates();
-    $this->drupalGet('admin/reports/status');
-    $this->assertNoRaw('Out of date');
-    $this->assertRaw('Mismatched entity and/or field definitions');
+    $this->assertNoRaw('Mismatched entity and/or field definitions');
   }
 
   /**
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleFieldTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleFieldTest.php
index ed30d4b..22476d7 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleFieldTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleFieldTest.php
@@ -93,7 +93,7 @@ public function testCustomBundleFieldUsage() {
     $entity->save();
     entity_test_delete_bundle('custom');
 
-    $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_bundle_field'));
+    $table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_bundle_field'), TRUE);
     $result = $this->database->select($table, 'f')
       ->condition('f.entity_id', $entity->id())
       ->condition('deleted', 1)
@@ -105,9 +105,10 @@ public function testCustomBundleFieldUsage() {
     $field_map = \Drupal::entityManager()->getFieldMap();
     $this->assertFalse(isset($field_map['entity_test']['custom_bundle_field']));
 
-    // @todo Test field purge and table deletion once supported. See
-    //   https://www.drupal.org/node/2282119.
-    // $this->assertFalse($this->database->schema()->tableExists($table), 'Custom field table was deleted');
+    // Purge field data, and check that the storage definition has been
+    // completely removed once the data is purged.
+    field_purge_batch(10);
+    $this->assertFalse($this->database->schema()->tableExists($table), 'Custom field table was deleted');
   }
 
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
index e2e4ae2..befcca4 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
@@ -391,51 +391,136 @@ public function testBundleFieldCreateDeleteWithExistingEntities() {
    * Tests deleting a base field when it has existing data.
    */
   public function testBaseFieldDeleteWithExistingData() {
+    /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
+    $storage = $this->entityManager->getStorage('entity_test_update');
+    $schema_handler = $this->database->schema();
+
     // Add the base field and run the update.
     $this->addBaseField();
     $this->entityDefinitionUpdateManager->applyUpdates();
 
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
+    $table_mapping = $storage->getTableMapping();
+    $storage_definition = $this->entityManager->getLastInstalledFieldStorageDefinitions('entity_test_update')['new_base_field'];
+
     // Save an entity with the base field populated.
-    $this->entityManager->getStorage('entity_test_update')->create(['new_base_field' => 'foo'])->save();
+    $entity = $storage->create(['new_base_field' => 'foo']);
+    $entity->save();
 
-    // Remove the base field and apply updates. It's expected to throw an
-    // exception.
-    // @todo Revisit that expectation once purging is implemented for
-    //   all fields: https://www.drupal.org/node/2282119.
+    // Remove the base field and apply updates.
     $this->removeBaseField();
-    try {
-      $this->entityDefinitionUpdateManager->applyUpdates();
-      $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.');
-    }
-    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
-      $this->pass('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.');
-    }
+    $this->entityDefinitionUpdateManager->applyUpdates();
+
+    // Check that the base field's column is deleted.
+    $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column deleted from shared table for new_base_field.');
+
+    // Check that a dedicated 'deleted' table was created for the deleted base
+    // field.
+    $dedicated_deleted_table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
+    $this->assertTrue($schema_handler->tableExists($dedicated_deleted_table_name), 'A dedicated table was created for the deleted new_base_field.');
+
+    // Check that the deleted field's data is preserved in the dedicated
+    // 'deleted' table.
+    $result = $this->database->select($dedicated_deleted_table_name, 't')
+      ->fields('t')
+      ->execute()
+      ->fetchAll();
+    $this->assertCount(1, $result);
+
+    $expected = [
+      'bundle' => $entity->bundle(),
+      'deleted' => '1',
+      'entity_id' => $entity->id(),
+      'revision_id' => $entity->id(),
+      'langcode' => $entity->language()->getId(),
+      'delta' => '0',
+      'new_base_field_value' => $entity->new_base_field->value,
+    ];
+    $this->assertSame($expected, (array) $result[0]);
+
+    // Check that the field storage definition is marked for purging.
+    $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
+    $this->assertArrayHasKey($storage_definition->getUniqueStorageIdentifier(), $deleted_storage_definitions, 'The base field is marked for purging.');
+
+    // Purge field data, and check that the storage definition has been
+    // completely removed once the data is purged.
+    field_purge_batch(10);
+    $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
+    $this->assertEmpty($deleted_storage_definitions, 'The base field has been deleted.');
+    $this->assertFalse($schema_handler->tableExists($dedicated_deleted_table_name), 'A dedicated table was created for the deleted new_base_field.');
   }
 
   /**
    * Tests deleting a bundle field when it has existing data.
    */
   public function testBundleFieldDeleteWithExistingData() {
+    /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
+    $storage = $this->entityManager->getStorage('entity_test_update');
+    $schema_handler = $this->database->schema();
+
     // Add the bundle field and run the update.
     $this->addBundleField();
     $this->entityDefinitionUpdateManager->applyUpdates();
 
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
+    $table_mapping = $storage->getTableMapping();
+    $storage_definition = $this->entityManager->getLastInstalledFieldStorageDefinitions('entity_test_update')['new_bundle_field'];
+
+    // Check that the bundle field has a dedicated table.
+    $dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
+    $this->assertTrue($schema_handler->tableExists($dedicated_table_name), 'The bundle field uses a dedicated table.');
+
     // Save an entity with the bundle field populated.
     entity_test_create_bundle('custom');
-    $this->entityManager->getStorage('entity_test_update')->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo'])->save();
+    $entity = $storage->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo']);
+    $entity->save();
 
-    // Remove the bundle field and apply updates. It's expected to throw an
-    // exception.
-    // @todo Revisit that expectation once purging is implemented for
-    //   all fields: https://www.drupal.org/node/2282119.
+    // Remove the bundle field and apply updates.
     $this->removeBundleField();
-    try {
-      $this->entityDefinitionUpdateManager->applyUpdates();
-      $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.');
-    }
-    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
-      $this->pass('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.');
-    }
+    $this->entityDefinitionUpdateManager->applyUpdates();
+
+    // Check that the table of the bundle field has been renamed to use a
+    // 'deleted' table name.
+    $this->assertFalse($schema_handler->tableExists($dedicated_table_name), 'The dedicated table of the bundle field no longer exists.');
+
+    $dedicated_deleted_table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
+    $this->assertTrue($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle fields has been renamed to use the "deleted" name.');
+
+    // Check that the deleted field's data is preserved in the dedicated
+    // 'deleted' table.
+    $result = $this->database->select($dedicated_deleted_table_name, 't')
+      ->fields('t')
+      ->execute()
+      ->fetchAll();
+    $this->assertCount(1, $result);
+
+    $expected = [
+      'bundle' => $entity->bundle(),
+      'deleted' => '1',
+      'entity_id' => $entity->id(),
+      'revision_id' => $entity->id(),
+      'langcode' => $entity->language()->getId(),
+      'delta' => '0',
+      'new_bundle_field_value' => $entity->new_bundle_field->value,
+    ];
+    $this->assertSame($expected, (array) $result[0]);
+
+    // Check that the field definition is marked for purging.
+    $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions();
+    $this->assertArrayHasKey($storage_definition->getUniqueIdentifier(), $deleted_field_definitions, 'The bundle field is marked for purging.');
+
+    // Check that the field storage definition is marked for purging.
+    $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
+    $this->assertArrayHasKey($storage_definition->getUniqueStorageIdentifier(), $deleted_storage_definitions, 'The bundle field storage is marked for purging.');
+
+    // Purge field data, and check that the storage definition has been
+    // completely removed once the data is purged.
+    field_purge_batch(10);
+    $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions();
+    $this->assertEmpty($deleted_field_definitions, 'The bundle field has been deleted.');
+    $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
+    $this->assertEmpty($deleted_storage_definitions, 'The bundle field storage has been deleted.');
+    $this->assertFalse($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle field has been removed.');
   }
 
   /**
diff --git a/core/tests/Drupal/KernelTests/Core/Field/FieldModuleUninstallValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Field/FieldModuleUninstallValidatorTest.php
deleted file mode 100644
index b9d545e..0000000
--- a/core/tests/Drupal/KernelTests/Core/Field/FieldModuleUninstallValidatorTest.php
+++ /dev/null
@@ -1,140 +0,0 @@
-<?php
-
-namespace Drupal\KernelTests\Core\Field;
-
-use Drupal\Core\Extension\ModuleUninstallValidatorException;
-use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\entity_test\FieldStorageDefinition;
-use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
-
-/**
- * Tests FieldModuleUninstallValidator functionality.
- *
- * @group Field
- */
-class FieldModuleUninstallValidatorTest extends EntityKernelTestBase {
-
-  /**
-   * The entity definition update manager.
-   *
-   * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
-   */
-  protected $entityDefinitionUpdateManager;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp() {
-    parent::setUp();
-    $this->installSchema('user', 'users_data');
-    $this->entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager');
-
-    // Setup some fields for entity_test_extra to create.
-    $definitions['extra_base_field'] = BaseFieldDefinition::create('string')
-      ->setName('extra_base_field')
-      ->setTargetEntityTypeId('entity_test')
-      ->setTargetBundle('entity_test');
-    $this->state->set('entity_test.additional_base_field_definitions', $definitions);
-    $definitions['extra_bundle_field'] = FieldStorageDefinition::create('string')
-      ->setName('extra_bundle_field')
-      ->setTargetEntityTypeId('entity_test')
-      ->setTargetBundle('entity_test');
-    $this->state->set('entity_test.additional_field_storage_definitions', $definitions);
-    $this->state->set('entity_test.entity_test.additional_bundle_field_definitions', $definitions);
-    $this->entityManager->clearCachedDefinitions();
-  }
-
-  /**
-   * Tests uninstall entity_test module with and without content for the field.
-   */
-  public function testUninstallingModule() {
-    // Test uninstall works fine without content.
-    $this->assertModuleInstallUninstall('entity_test_extra');
-
-    // Test uninstalling works fine with content having no field values.
-    $entity = $this->entityManager->getStorage('entity_test')->create([
-      'name' => $this->randomString(),
-    ]);
-    $entity->save();
-    $this->assertModuleInstallUninstall('entity_test_extra');
-    $entity->delete();
-
-    // Verify uninstall works fine without content again.
-    $this->assertModuleInstallUninstall('entity_test_extra');
-    // Verify uninstalling entity_test is not possible when there is content for
-    // the base field.
-    $this->enableModules(['entity_test_extra']);
-    $this->entityDefinitionUpdateManager->applyUpdates();
-    $entity = $this->entityManager->getStorage('entity_test')->create([
-      'name' => $this->randomString(),
-      'extra_base_field' => $this->randomString(),
-    ]);
-    $entity->save();
-
-    try {
-      $message = 'Module uninstallation fails as the module provides a base field which has content.';
-      $this->getModuleInstaller()->uninstall(['entity_test_extra']);
-      $this->fail($message);
-    }
-    catch (ModuleUninstallValidatorException $e) {
-      $this->pass($message);
-      $this->assertEqual($e->getMessage(), 'The following reasons prevent the modules from being uninstalled: There is data for the field extra_base_field on entity type Test entity');
-    }
-
-    // Verify uninstalling entity_test is not possible when there is content for
-    // the bundle field.
-    $entity->delete();
-    $this->assertModuleInstallUninstall('entity_test_extra');
-    $this->enableModules(['entity_test_extra']);
-    $this->entityDefinitionUpdateManager->applyUpdates();
-    $entity = $this->entityManager->getStorage('entity_test')->create([
-      'name' => $this->randomString(),
-      'extra_bundle_field' => $this->randomString(),
-    ]);
-    $entity->save();
-    try {
-      $this->getModuleInstaller()->uninstall(['entity_test_extra']);
-      $this->fail('Module uninstallation fails as the module provides a bundle field which has content.');
-    }
-    catch (ModuleUninstallValidatorException $e) {
-      $this->pass('Module uninstallation fails as the module provides a bundle field which has content.');
-    }
-  }
-
-  /**
-   * Asserts the given module can be installed and uninstalled.
-   *
-   * @param string $module_name
-   *   The module to install and uninstall.
-   */
-  protected function assertModuleInstallUninstall($module_name) {
-    // Install the module if it is not installed yet.
-    if (!\Drupal::moduleHandler()->moduleExists($module_name)) {
-      $this->enableModules([$module_name]);
-    }
-    $this->entityDefinitionUpdateManager->applyUpdates();
-    $this->assertTrue($this->getModuleHandler()->moduleExists($module_name), $module_name . ' module is enabled.');
-    $this->getModuleInstaller()->uninstall([$module_name]);
-    $this->entityDefinitionUpdateManager->applyUpdates();
-    $this->assertFalse($this->getModuleHandler()->moduleExists($module_name), $module_name . ' module is disabled.');
-  }
-
-  /**
-   * Returns the ModuleHandler.
-   *
-   * @return \Drupal\Core\Extension\ModuleHandlerInterface
-   */
-  protected function getModuleHandler() {
-    return $this->container->get('module_handler');
-  }
-
-  /**
-   * Returns the ModuleInstaller.
-   *
-   * @return \Drupal\Core\Extension\ModuleInstallerInterface
-   */
-  protected function getModuleInstaller() {
-    return $this->container->get('module_installer');
-  }
-
-}
