diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
index 257fdf0fdb..c88fddf5fe 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
@@ -225,7 +225,8 @@ protected function doLoadMultiple(array $ids = NULL) {
   protected function doCreate(array $values) {
     // Set default language to current language if not provided.
     $values += [$this->langcodeKey => $this->languageManager->getCurrentLanguage()->getId()];
-    $entity = new $this->entityClass($values, $this->entityTypeId);
+    $entity_class = $this->getEntityClass();
+    $entity = new $entity_class($values, $this->entityTypeId);
 
     return $entity;
   }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index 80500b577c..995edeb3c4 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -64,6 +64,45 @@ public function __construct(EntityTypeInterface $entity_type, EntityManagerInter
   }
 
   /**
+   * A list of bundle classes.
+   *
+   * @return array
+   *   An associative array containing bundle class names, where the key is the
+   *   bundle machine name.
+   */
+  public static function bundleClasses() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function create(array $values = []) {
+    // In some cases the entity bundle may be provided by the entity class'
+    // ::preCreate method. If a bundle is truly required an exception will be
+    // thrown in the ::doCreate method so there's no need to throw one here.
+    $bundle = $this->getBundleFromValues($values, FALSE);
+    $entity_class = $this->getEntityClass($bundle);
+    $entity_class::preCreate($this, $values);
+
+    // Assign a new UUID if there is none yet.
+    if ($this->uuidKey && $this->uuidService && !isset($values[$this->uuidKey])) {
+      $values[$this->uuidKey] = $this->uuidService->generate();
+    }
+
+    $entity = $this->doCreate($values);
+    $entity->enforceIsNew();
+
+    $entity->postCreate($this);
+
+    // Modules might need to add or change the data initially held by the new
+    // entity object, for instance to fill-in default values.
+    $this->invokeHook('create', $entity);
+
+    return $entity;
+  }
+
+  /**
    * {@inheritdoc}
    */
   public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
@@ -79,34 +118,88 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    * {@inheritdoc}
    */
   protected function doCreate(array $values) {
-    // We have to determine the bundle first.
-    $bundle = FALSE;
-    if ($this->bundleKey) {
-      if (!isset($values[$this->bundleKey])) {
-        throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
-      }
+    $bundle = $this->getBundleFromValues($values);
+    $entity_class = $this->getEntityClass($bundle);
+    $entity = new $entity_class([], $this->entityTypeId, $bundle);
+    $this->initFieldValues($entity, $values);
+    return $entity;
+  }
 
-      // Normalize the bundle value. This is an optimized version of
-      // \Drupal\Core\Field\FieldInputValueNormalizerTrait::normalizeValue()
-      // because we just need the scalar value.
-      $bundle_value = $values[$this->bundleKey];
-      if (!is_array($bundle_value)) {
-        // The bundle value is a scalar, use it as-is.
-        $bundle = $bundle_value;
-      }
-      elseif (is_numeric(array_keys($bundle_value)[0])) {
-        // The bundle value is a field item list array, keyed by delta.
-        $bundle = reset($bundle_value[0]);
+  /**
+   * Retrieves the bundle name for a provided class name.
+   *
+   * @param string $class_name
+   *   The class name to check.
+   *
+   * @return string|null
+   *   The bundle name of the class provided or NULL if unable to determine the
+   *   bundle from the provided class.
+   */
+  public function getBundleFromClass($class_name) {
+    $bundle_classes = array_flip(static::bundleClasses());
+    return isset($bundle_classes[$class_name]) ? $bundle_classes[$class_name] : NULL;
+  }
+
+  /**
+   * Retrieves the bundle from an array of values.
+   *
+   * @param array $values
+   *   An array of values to set, keyed by property name.
+   * @param bool $throw_exception
+   *   Flag indicating whether to throw an exception if corresponding bundle
+   *   cannot be found and is expected.
+   *
+   * @return string|null
+   *   The bundle or NULL if not set.
+   *
+   * @throws \Drupal\Core\Entity\EntityStorageException when a corresponding
+   *   bundle cannot be found and is expected.
+   */
+  public function getBundleFromValues(array $values, $throw_exception = TRUE) {
+    $bundle = NULL;
+    if ($this->bundleKey) {
+      if (isset($values[$this->bundleKey])) {
+        // Normalize the bundle value. This is an optimized version of
+        // \Drupal\Core\Field\FieldInputValueNormalizerTrait::normalizeValue()
+        // because we just need the scalar value.
+        $bundle_value = $values[$this->bundleKey];
+        if (!is_array($bundle_value)) {
+          // The bundle value is a scalar, use it as-is.
+          $bundle = $bundle_value;
+        }
+        elseif (is_numeric(array_keys($bundle_value)[0])) {
+          // The bundle value is a field item list array, keyed by delta.
+          $bundle = reset($bundle_value[0]);
+        }
+        else {
+          // The bundle value is a field item array, keyed by the field's main
+          // property name.
+          $bundle = reset($bundle_value);
+        }
       }
-      else {
-        // The bundle value is a field item array, keyed by the field's main
-        // property name.
-        $bundle = reset($bundle_value);
+      elseif ($throw_exception) {
+        throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
       }
     }
-    $entity = new $this->entityClass([], $this->entityTypeId, $bundle);
-    $this->initFieldValues($entity, $values);
-    return $entity;
+    return $bundle;
+  }
+
+  /**
+   * Retrieves the entity bundle key.
+   *
+   * @return bool|string
+   *   The entity bundle key.
+   */
+  public function getBundleKey() {
+    return $this->bundleKey;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntityClass($bundle = NULL) {
+    $bundle_classes = static::bundleClasses();
+    return isset($bundle_classes[$bundle]) ? $bundle_classes[$bundle] : parent::getEntityClass($bundle);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 84991bc609..134b46814f 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -543,7 +543,14 @@ public static function loadMultiple(array $ids = NULL) {
   public static function create(array $values = []) {
     $entity_type_repository = \Drupal::service('entity_type.repository');
     $entity_type_manager = \Drupal::entityTypeManager();
-    $storage = $entity_type_manager->getStorage($entity_type_repository->getEntityTypeFromClass(get_called_class()));
+    $class_name = get_called_class();
+    $storage = $entity_type_manager->getStorage($entity_type_repository->getEntityTypeFromClass($class_name));
+
+    // Always explicitly specify the bundle for a subclassed entity.
+    if ($storage instanceof ContentEntityStorageBase && ($bundle = $storage->getBundleFromClass($class_name))) {
+      $values[$storage->getBundleKey()] = $bundle;
+    }
+
     return $storage->create($values);
   }
 
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
index d7263dc316..47ccf8f1b7 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
@@ -105,6 +105,20 @@ public function __construct(EntityTypeInterface $entity_type, MemoryCacheInterfa
   }
 
   /**
+   * Retrieves the class name used to create the entity.
+   *
+   * @param string $bundle
+   *   A specific entity type bundle identifier. Note: depending on how the
+   *   entity is configured, a bundle may not be provided.
+   *
+   * @return string
+   *   A class name.
+   */
+  protected function getEntityClass($bundle = NULL) {
+    return $this->entityClass;
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function getEntityTypeId() {
@@ -210,7 +224,7 @@ protected function invokeHook($hook, EntityInterface $entity) {
    * {@inheritdoc}
    */
   public function create(array $values = []) {
-    $entity_class = $this->entityClass;
+    $entity_class = $this->getEntityClass();
     $entity_class::preCreate($this, $values);
 
     // Assign a new UUID if there is none yet.
@@ -239,7 +253,8 @@ public function create(array $values = []) {
    * @return \Drupal\Core\Entity\EntityInterface
    */
   protected function doCreate(array $values) {
-    return new $this->entityClass($values, $this->entityTypeId);
+    $entity_class = $this->getEntityClass();
+    return new $entity_class($values, $this->entityTypeId);
   }
 
   /**
@@ -330,17 +345,31 @@ public function loadMultiple(array $ids = NULL) {
    *   Associative array of query results, keyed on the entity ID.
    */
   protected function postLoad(array &$entities) {
-    $entity_class = $this->entityClass;
-    $entity_class::postLoad($this, $entities);
-    // Call hook_entity_load().
-    foreach ($this->moduleHandler()->getImplementations('entity_load') as $module) {
-      $function = $module . '_entity_load';
-      $function($entities, $this->entityTypeId);
+    $entity_classes = [];
+
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
+    foreach ($entities as $id => &$entity) {
+      $entity_class = get_class($entity);
+      if (!isset($entity_classes[$entity_class])) {
+        $entity_classes[$entity_class] = [];
+      }
+      $entity_classes[$entity_class][$id] = &$entity;
     }
-    // Call hook_TYPE_load().
-    foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load') as $module) {
-      $function = $module . '_' . $this->entityTypeId . '_load';
-      $function($entities);
+
+    foreach ($entity_classes as $entity_class => &$items) {
+      $entity_class::postLoad($this, $entities);
+
+      // Call hook_entity_load().
+      foreach ($this->moduleHandler()->getImplementations('entity_load') as $module) {
+        $function = $module . '_entity_load';
+        $function($items, $this->entityTypeId);
+      }
+
+      // Call hook_TYPE_load().
+      foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load') as $module) {
+        $function = $module . '_' . $this->entityTypeId . '_load';
+        $function($items);
+      }
     }
   }
 
@@ -356,7 +385,8 @@ protected function postLoad(array &$entities) {
   protected function mapFromStorageRecords(array $records) {
     $entities = [];
     foreach ($records as $record) {
-      $entity = new $this->entityClass($record, $this->entityTypeId);
+      $entity_class = $this->getEntityClass();
+      $entity = new $entity_class($record, $this->entityTypeId);
       $entities[$entity->id()] = $entity;
     }
     return $entities;
@@ -389,21 +419,33 @@ public function delete(array $entities) {
       $keyed_entities[$entity->id()] = $entity;
     }
 
-    // Allow code to run before deleting.
-    $entity_class = $this->entityClass;
-    $entity_class::preDelete($this, $keyed_entities);
-    foreach ($keyed_entities as $entity) {
-      $this->invokeHook('predelete', $entity);
+    $entity_classes = [];
+
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
+    foreach ($keyed_entities as $id => $entity) {
+      $entity_class = get_class($entity);
+      if (!isset($entity_classes[$entity_class])) {
+        $entity_classes[$entity_class] = [];
+      }
+      $entity_classes[$entity_class][$id] = $entity;
     }
 
-    // Perform the delete and reset the static cache for the deleted entities.
-    $this->doDelete($keyed_entities);
-    $this->resetCache(array_keys($keyed_entities));
+    // Allow code to run before deleting.
+    foreach ($entity_classes as $entity_class => &$items) {
+      $entity_class::preDelete($this, $items);
+      foreach ($items as $entity) {
+        $this->invokeHook('predelete', $entity);
+      }
 
-    // Allow code to run after deleting.
-    $entity_class::postDelete($this, $keyed_entities);
-    foreach ($keyed_entities as $entity) {
-      $this->invokeHook('delete', $entity);
+      // Perform the delete and reset the static cache for the deleted entities.
+      $this->doDelete($items);
+      $this->resetCache(array_keys($items));
+
+      // Allow code to run after deleting.
+      $entity_class::postDelete($this, $items);
+      foreach ($items as $entity) {
+        $this->invokeHook('delete', $entity);
+      }
     }
   }
 
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeRepository.php b/core/lib/Drupal/Core/Entity/EntityTypeRepository.php
index d89b75e3ff..97d4ab2c7d 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeRepository.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeRepository.php
@@ -80,8 +80,11 @@ public function getEntityTypeFromClass($class_name) {
 
     $same_class = 0;
     $entity_type_id = NULL;
-    foreach ($this->entityTypeManager->getDefinitions() as $entity_type) {
-      if ($entity_type->getOriginalClass() == $class_name  || $entity_type->getClass() == $class_name) {
+    $definitions = $this->entityTypeManager->getDefinitions();
+
+    // Attempt to find an exact entity class name match.
+    foreach ($definitions as $entity_type) {
+      if ($class_name === $entity_type->getClass() || $class_name === $entity_type->getOriginalClass()) {
         $entity_type_id = $entity_type->id();
         if ($same_class++) {
           throw new AmbiguousEntityClassException($class_name);
@@ -89,6 +92,18 @@ public function getEntityTypeFromClass($class_name) {
       }
     }
 
+    // Attempt to find a subclassed entity class name match.
+    if (!$entity_type_id) {
+      foreach ($definitions as $entity_type) {
+        if (is_subclass_of($class_name, $entity_type->getClass()) || is_subclass_of($class_name, $entity_type->getOriginalClass())) {
+          $entity_type_id = $entity_type->id();
+          if ($same_class++) {
+            throw new AmbiguousEntityClassException($class_name);
+          }
+        }
+      }
+    }
+
     // Return the matching entity type ID if there is one.
     if ($entity_type_id) {
       $this->classNameEntityTypeMap[$class_name] = $entity_type_id;
diff --git a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
index 113f630787..959cf1e93a 100644
--- a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php
@@ -93,7 +93,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
   public function doCreate(array $values = []) {
     // Set default language to site default if not provided.
     $values += [$this->getEntityType()->getKey('langcode') => $this->languageManager->getDefaultLanguage()->getId()];
-    $entity = new $this->entityClass($values, $this->entityTypeId);
+    $entity_class = $this->getEntityClass();
+    $entity = new $entity_class($values, $this->entityTypeId);
 
     // @todo This is handled by ContentEntityStorageBase, which assumes
     //   FieldableEntityInterface. The current approach in
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index 261ff96b71..b932682a2b 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -468,7 +468,8 @@ protected function mapFromStorageRecords(array $records, $load_from_revision = F
     foreach ($values as $id => $entity_values) {
       $bundle = $this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : FALSE;
       // Turn the record into an entity class.
-      $entities[$id] = new $this->entityClass($entity_values, $this->entityTypeId, $bundle, array_keys($translations[$id]));
+      $entity_class = $this->getEntityClass($bundle);
+      $entities[$id] = new $entity_class($entity_values, $this->entityTypeId, $bundle, array_keys($translations[$id]));
     }
 
     return $entities;
diff --git a/core/modules/system/tests/modules/entity_test_subclass/entity_test_subclass.info.yml b/core/modules/system/tests/modules/entity_test_subclass/entity_test_subclass.info.yml
new file mode 100644
index 0000000000..f6c5769eb6
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_subclass/entity_test_subclass.info.yml
@@ -0,0 +1,8 @@
+name: 'Entity Subclass Test'
+type: module
+description: 'Support module for testing entity type subclassing.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+  - entity_test
diff --git a/core/modules/system/tests/modules/entity_test_subclass/entity_test_subclass.module b/core/modules/system/tests/modules/entity_test_subclass/entity_test_subclass.module
new file mode 100644
index 0000000000..fdb86f809d
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_subclass/entity_test_subclass.module
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Support module for testing entity type subclassing.
+ */
+
+/**
+ * Implements hook_entity_type_alter().
+ */
+function entity_test_subclass_entity_type_alter(array &$entity_types) {
+  /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
+  $entity_types['entity_test']->setStorageClass('\Drupal\entity_test_subclass\EntityTestSubclassStorage');
+}
diff --git a/core/modules/system/tests/modules/entity_test_subclass/src/Entity/EntityTestSubclass.php b/core/modules/system/tests/modules/entity_test_subclass/src/Entity/EntityTestSubclass.php
new file mode 100644
index 0000000000..c469d280f0
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_subclass/src/Entity/EntityTestSubclass.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Drupal\entity_test_subclass\Entity;
+
+use Drupal\entity_test\Entity\EntityTest;
+
+/**
+ * Provides a subclass for the entity_test entity type.
+ */
+class EntityTestSubclass extends EntityTest {
+}
diff --git a/core/modules/system/tests/modules/entity_test_subclass/src/EntityTestSubclassStorage.php b/core/modules/system/tests/modules/entity_test_subclass/src/EntityTestSubclassStorage.php
new file mode 100644
index 0000000000..25dac61093
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_subclass/src/EntityTestSubclassStorage.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\entity_test_subclass;
+
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\entity_test_subclass\Entity\EntityTestSubclass;
+
+/**
+ * Class EntityTestSubclassStorage.
+ */
+class EntityTestSubclassStorage extends SqlContentEntityStorage {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function bundleClasses() {
+    return [
+      'subclass' => EntityTestSubclass::class,
+    ];
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntitySubclassTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntitySubclassTest.php
new file mode 100644
index 0000000000..aafbdb20d0
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntitySubclassTest.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\entity_test_subclass\Entity\EntityTestSubclass;
+
+/**
+ * Tests adding a custom bundle field.
+ *
+ * @group Entity
+ */
+class EntitySubclassTest extends EntityKernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['entity_test_subclass'];
+
+  /**
+   * The entity storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->storage = $this->entityManager->getStorage('entity_test');
+  }
+
+  /**
+   * Tests making use of a custom bundle field.
+   */
+  public function testEntitySubclass() {
+    entity_test_create_bundle('subclass');
+
+    // Verify statically created subclassed entity returns correct class.
+    $entity = EntityTestSubclass::create();
+    $this->assertTrue($entity instanceof EntityTestSubclass);
+
+    // Verify statically created subclassed entity returns correct bundle.
+    $entity = EntityTestSubclass::create(['type' => 'custom']);
+    $this->assertTrue($entity instanceof EntityTestSubclass);
+    $this->assertEquals('subclass', $entity->bundle());
+
+    // Verify that the entity storage creates the entity using the proper class.
+    $entity = $this->storage->create(['type' => 'subclass']);
+    $this->assertTrue($entity instanceof EntityTestSubclass);
+
+    // Verify that loading an entity returns the proper class.
+    $entity->save();
+    $id = $entity->id();
+    $this->storage->resetCache();
+    $entity = $this->storage->load($id);
+    $this->assertTrue($entity instanceof EntityTestSubclass);
+  }
+
+}
