diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
index 5569dd0989..4346a04ac3 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/BundleEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/BundleEntityStorageInterface.php
new file mode 100644
index 0000000000..9ba915fabd
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/BundleEntityStorageInterface.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\Core\Entity;
+
+/**
+ * A storage that supports entities with bundle specific classes.
+ */
+interface BundleEntityStorageInterface {
+
+  /**
+   * 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.
+   *
+   * @throws \Drupal\Core\Entity\Exception\AmbiguousBundleClassException
+   *   Thrown when multiple bundles are using the provided class.
+   */
+  public function getBundleFromClass($class_name);
+
+  /**
+   * Retrieves the entity bundle key.
+   *
+   * @return bool|string
+   *   The entity bundle key.
+   */
+  public function getBundleKey();
+
+}
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index 5ca737ffbf..e1e0395a7c 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -1125,6 +1125,23 @@ public function __unset($name) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(array $values = []) {
+    $entity_type_repository = \Drupal::service('entity_type.repository');
+    $entity_type_manager = \Drupal::entityTypeManager();
+    $class_name = get_called_class();
+    $storage = $entity_type_manager->getStorage($entity_type_repository->getEntityTypeFromClass($class_name));
+
+    // Always explicitly specify the bundle if the entity has a bundle class.
+    if ($storage instanceof ContentEntityStorageBase && ($bundle = $storage->getBundleFromClass($class_name))) {
+      $values[$storage->getBundleKey()] = $bundle;
+    }
+
+    return $storage->create($values);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index 23e1f32c56..a87768c2dc 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -6,6 +6,8 @@
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
 use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
+use Drupal\Core\Entity\Exception\AmbiguousBundleClassException;
+use Drupal\Core\Entity\Exception\BundleClassInheritanceException;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Language\LanguageInterface;
@@ -15,7 +17,7 @@
 /**
  * Base class for content entity storage handlers.
  */
-abstract class ContentEntityStorageBase extends EntityStorageBase implements ContentEntityStorageInterface, DynamicallyFieldableEntityStorageInterface {
+abstract class ContentEntityStorageBase extends EntityStorageBase implements ContentEntityStorageInterface, DynamicallyFieldableEntityStorageInterface, BundleEntityStorageInterface {
   use DeprecatedServicePropertyTrait;
 
   /**
@@ -84,6 +86,34 @@ public function __construct(EntityTypeInterface $entity_type, EntityFieldManager
     $this->entityTypeBundleInfo = $entity_type_bundle_info;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function create(array $values = []) {
+    // In some cases the entity bundle may be provided by the ::preCreate()
+    // method in the entity class. If a bundle is truly required an exception
+    // will be thrown in ::doCreate() 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}
    */
@@ -101,34 +131,105 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    * {@inheritdoc}
    */
   protected function doCreate(array $values) {
-    // We have to determine the bundle first.
-    $bundle = FALSE;
+    $bundle = $this->getBundleFromValues($values);
+    $entity_class = $this->getEntityClass($bundle);
+    $entity = new $entity_class([], $this->entityTypeId, $bundle);
+    $this->initFieldValues($entity, $values);
+    return $entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBundleFromClass($class_name) {
+    $class_bundles = array_filter($this->entityTypeBundleInfo->getBundleInfo($this->entityTypeId), function ($bundle_info) use ($class_name) {
+      return !empty($bundle_info['class']) && $bundle_info['class'] === $class_name;
+    });
+
+    if (empty($class_bundles)) {
+      return NULL;
+    }
+
+    if (count($class_bundles) > 1) {
+      throw new AmbiguousBundleClassException($class_name);
+    }
+
+    reset($class_bundles);
+    return key($class_bundles);
+  }
+
+  /**
+   * 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.
+   */
+  protected function getBundleFromValues(array $values, $throw_exception = TRUE) {
+    $bundle = NULL;
     if ($this->bundleKey) {
-      if (!isset($values[$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);
+        }
+      }
+      elseif ($throw_exception) {
         throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
       }
+    }
+    return $bundle;
+  }
 
-      // 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);
+  /**
+   * {@inheritdoc}
+   */
+  public function getBundleKey() {
+    return $this->bundleKey;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntityClass($bundle = NULL) {
+    $entity_class = parent::getEntityClass();
+
+    if (!empty($bundle)) {
+      // Return the bundle class if it has been defined for this bundle.
+      $bundle_info = $this->entityTypeBundleInfo->getBundleInfo($this->entityTypeId);
+      $bundle_class = $bundle_info[$bundle]['class'] ?? NULL;
+
+      // Bundle classes should extend the main entity class.
+      if ($bundle_class) {
+        if (!is_subclass_of($bundle_class, $entity_class)) {
+          throw new BundleClassInheritanceException($bundle_class, $entity_class);
+        }
+        return $bundle_class;
       }
     }
-    $entity = new $this->entityClass([], $this->entityTypeId, $bundle);
-    $this->initFieldValues($entity, $values);
-    return $entity;
+
+    return $entity_class;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
index 067b08acb5..89f1316fbb 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
@@ -104,6 +104,20 @@ public function __construct(EntityTypeInterface $entity_type, MemoryCacheInterfa
     $this->memoryCacheTag = 'entity.memory_cache:' . $this->entityTypeId;
   }
 
+  /**
+   * Retrieves the class name used to create the entity.
+   *
+   * @param string|null $bundle
+   *   (optional) A specific entity type bundle identifier. Can be omitted in
+   *   the case of entity types without bundles, like User.
+   *
+   * @return string
+   *   The class name.
+   */
+  protected function getEntityClass($bundle = NULL) {
+    return $this->entityClass;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -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);
   }
 
   /**
@@ -358,17 +373,22 @@ protected function preLoad(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);
-    }
-    // Call hook_TYPE_load().
-    foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load') as $module) {
-      $function = $module . '_' . $this->entityTypeId . '_load';
-      $function($entities);
+    $entity_classes = $this->getEntityClasses($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);
+      }
     }
   }
 
@@ -384,7 +404,9 @@ 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();
+      /* @var $entity \Drupal\Core\Entity\EntityInterface */
+      $entity = new $entity_class($record, $this->entityTypeId);
       $entities[$entity->id()] = $entity;
     }
     return $entities;
@@ -399,6 +421,7 @@ protected function mapFromStorageRecords(array $records) {
    *   The entity being saved.
    *
    * @return bool
+   *   TRUE if this entity exists in storage, FALSE otherwise.
    */
   abstract protected function has($id, EntityInterface $entity);
 
@@ -417,21 +440,24 @@ public function delete(array $entities) {
       $keyed_entities[$entity->id()] = $entity;
     }
 
+    $entity_classes = $this->getEntityClasses($keyed_entities);
+
     // 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);
-    }
+    foreach ($entity_classes as $entity_class => &$items) {
+      $entity_class::preDelete($this, $items);
+      foreach ($items as $entity) {
+        $this->invokeHook('predelete', $entity);
+      }
 
-    // Perform the delete and reset the static cache for the deleted entities.
-    $this->doDelete($keyed_entities);
-    $this->resetCache(array_keys($keyed_entities));
+      // 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, $keyed_entities);
-    foreach ($keyed_entities as $entity) {
-      $this->invokeHook('delete', $entity);
+      // Allow code to run after deleting.
+      $entity_class::postDelete($this, $items);
+      foreach ($items as $entity) {
+        $this->invokeHook('delete', $entity);
+      }
     }
   }
 
@@ -614,4 +640,27 @@ public function getAggregateQuery($conjunction = 'AND') {
    */
   abstract protected function getQueryServiceName();
 
+  /**
+   * Indexes the given array of entities by their class name and ID.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface[] $entities
+   *   The array of entities to index.
+   *
+   * @return array
+   *   An array of the passed-in entities, indexed by their class name and ID.
+   */
+  protected function getEntityClasses(array $entities) {
+    $entity_classes = [];
+
+    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;
+    }
+
+    return $entity_classes;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeRepository.php b/core/lib/Drupal/Core/Entity/EntityTypeRepository.php
index f63965a8b3..5930a249d1 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeRepository.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeRepository.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Entity;
 
+use Drupal\Core\Entity\Exception\AmbiguousBundleClassException;
 use Drupal\Core\Entity\Exception\AmbiguousEntityClassException;
 use Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -80,7 +81,8 @@ public function getEntityTypeFromClass($class_name) {
 
     $same_class = 0;
     $entity_type_id = NULL;
-    foreach ($this->entityTypeManager->getDefinitions() as $entity_type) {
+    $definitions = $this->entityTypeManager->getDefinitions();
+    foreach ($definitions as $entity_type) {
       if ($entity_type->getOriginalClass() == $class_name  || $entity_type->getClass() == $class_name) {
         $entity_type_id = $entity_type->id();
         if ($same_class++) {
@@ -89,6 +91,20 @@ public function getEntityTypeFromClass($class_name) {
       }
     }
 
+    // If no match was found check if it is a bundle class. This needs to be in
+    // a separate loop to avoid false positives, since an entity class can
+    // subclass another entity class.
+    if (!$entity_type_id) {
+      foreach ($definitions as $entity_type) {
+        if (is_subclass_of($class_name, $entity_type->getOriginalClass()) || is_subclass_of($class_name, $entity_type->getClass())) {
+          $entity_type_id = $entity_type->id();
+          if ($same_class++) {
+            throw new AmbiguousBundleClassException($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/EntityTypeRepositoryInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeRepositoryInterface.php
index 80b96e477a..5f4c2ccf15 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeRepositoryInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeRepositoryInterface.php
@@ -34,6 +34,8 @@ public function getEntityTypeLabels($group = FALSE);
    *
    * @throws \Drupal\Core\Entity\Exception\AmbiguousEntityClassException
    *   Thrown when multiple subclasses correspond to the called class.
+   * @throws \Drupal\Core\Entity\Exception\AmbiguousBundleClassException
+   *   Thrown when multiple subclasses correspond to the called bundle class.
    * @throws \Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException
    *   Thrown when no entity class corresponds to the called class.
    *
diff --git a/core/lib/Drupal/Core/Entity/Exception/AmbiguousBundleClassException.php b/core/lib/Drupal/Core/Entity/Exception/AmbiguousBundleClassException.php
new file mode 100644
index 0000000000..d830880f71
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Exception/AmbiguousBundleClassException.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\Core\Entity\Exception;
+
+/**
+ * Exception thrown if a bundle class is defined for multiple bundles.
+ *
+ * @see \Drupal\Core\Entity\ContentEntityStorageBase::getBundleFromClass()
+ */
+class AmbiguousBundleClassException extends \Exception {
+
+  /**
+   * Constructs an AmbiguousBundleClassException.
+   *
+   * @param string $class
+   *   The bundle class which is defined for multiple bundles.
+   */
+  public function __construct(string $class) {
+    $message = sprintf('Multiple bundles are using the bundle class %s.', $class);
+    parent::__construct($message);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Exception/BundleClassInheritanceException.php b/core/lib/Drupal/Core/Entity/Exception/BundleClassInheritanceException.php
new file mode 100644
index 0000000000..4f0d1fd2ba
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Exception/BundleClassInheritanceException.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\Core\Entity\Exception;
+
+/**
+ * Exception thrown if a bundle class does not extend the main entity class.
+ *
+ * @see \Drupal\Core\Entity\ContentEntityStorageBase::getEntityClass()
+ */
+class BundleClassInheritanceException extends \Exception {
+
+  /**
+   * Constructs a BundleClassInheritanceException.
+   *
+   * @param string $bundle_class
+   *   The bundle class which should extend the entity class.
+   * @param string $entity_class
+   *   The entity class which should be extended.
+   */
+  public function __construct(string $bundle_class, string $entity_class) {
+    $message = sprintf('Bundle class %s does not extend entity class %s.', $bundle_class, $entity_class);
+    parent::__construct($message);
+  }
+
+}
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 083ce84ccf..75508b6c69 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -527,7 +527,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/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php
index d04baffa25..7bc36a7620 100644
--- a/core/lib/Drupal/Core/Entity/entity.api.php
+++ b/core/lib/Drupal/Core/Entity/entity.api.php
@@ -828,6 +828,8 @@ function hook_entity_view_mode_info_alter(&$view_modes) {
  *     the entity type and the bundle, the one for the bundle is used.
  *   - translatable: (optional) A boolean value specifying whether this bundle
  *     has translation support enabled. Defaults to FALSE.
+ *   - class: (optional) The fully qualified class name for this bundle. If
+ *     omitted the class from the entity type definition will be used.
  *
  * @see \Drupal\Core\Entity\EntityTypeBundleInfo::getBundleInfo()
  * @see hook_entity_bundle_info_alter()
@@ -904,6 +904,8 @@ function hook_entity_bundle_info() {
  */
 function hook_entity_bundle_info_alter(&$bundles) {
   $bundles['user']['user']['label'] = t('Full account');
   $bundles['user']['user']['label_count']['search results'] = t("1 user is registered\x03@count users are registered");
+  // Override the bundle class for the "article" node type in a custom module.
+  $bundles['node']['article']['class'] = 'Drupal\mymodule\Entity\Article';
 }
 
 /**
diff --git a/core/modules/system/tests/modules/entity_test_bundle_class/entity_test_bundle_class.info.yml b/core/modules/system/tests/modules/entity_test_bundle_class/entity_test_bundle_class.info.yml
new file mode 100644
index 0000000000..f295db0241
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_bundle_class/entity_test_bundle_class.info.yml
@@ -0,0 +1,8 @@
+name: 'Entity Bundle Class Test'
+type: module
+description: 'Support module for testing entity bundle classes.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+  - entity_test
diff --git a/core/modules/system/tests/modules/entity_test_bundle_class/entity_test_bundle_class.module b/core/modules/system/tests/modules/entity_test_bundle_class/entity_test_bundle_class.module
new file mode 100644
index 0000000000..576143a4c1
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_bundle_class/entity_test_bundle_class.module
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Support module for testing entity bundle classes.
+ */
+
+use Drupal\entity_test_bundle_class\Entity\EntityTestBundleClass;
+use Drupal\entity_test_bundle_class\Entity\NonInheritingBundleClass;
+
+/**
+ * Implements hook_entity_bundle_info_alter().
+ */
+function entity_test_bundle_class_entity_bundle_info_alter(&$bundles) {
+  if (!empty($bundles['entity_test']['bundle_class'])) {
+    $bundles['entity_test']['bundle_class']['class'] = EntityTestBundleClass::class;
+  }
+
+  if (\Drupal::state()->get('entity_test_bundle_class_enable_ambiguous_entity_types', FALSE)) {
+    $bundles['entity_test']['entity_test_no_label']['class'] = EntityTestBundleClass::class;
+  }
+
+  if (\Drupal::state()->get('entity_test_bundle_class_non_inheriting', FALSE)) {
+    $bundles['entity_test']['bundle_class']['class'] = NonInheritingBundleClass::class;
+  }
+}
diff --git a/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/EntityTestBundleClass.php b/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/EntityTestBundleClass.php
new file mode 100644
index 0000000000..56291da832
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/EntityTestBundleClass.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Drupal\entity_test_bundle_class\Entity;
+
+use Drupal\entity_test\Entity\EntityTest;
+
+/**
+ * The bundle class for the bundle_class bundle of the entity_test entity.
+ */
+class EntityTestBundleClass extends EntityTest {
+}
diff --git a/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/NonInheritingBundleClass.php b/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/NonInheritingBundleClass.php
new file mode 100644
index 0000000000..1ad9b67fd5
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/NonInheritingBundleClass.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Drupal\entity_test_bundle_class\Entity;
+
+/**
+ * An invalid bundle class which does not inherit the main entity class.
+ */
+class NonInheritingBundleClass {
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/BundleClassTest.php b/core/tests/Drupal/KernelTests/Core/Entity/BundleClassTest.php
new file mode 100644
index 0000000000..2713c4841a
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/BundleClassTest.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\Exception\AmbiguousBundleClassException;
+use Drupal\Core\Entity\Exception\BundleClassInheritanceException;
+use Drupal\entity_test_bundle_class\Entity\EntityTestBundleClass;
+
+/**
+ * Tests entity bundle classes.
+ *
+ * @group Entity
+ */
+class BundleClassTest extends EntityKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['entity_test_bundle_class'];
+
+  /**
+   * The entity storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->storage = $this->entityTypeManager->getStorage('entity_test');
+  }
+
+  /**
+   * Tests making use of a custom bundle field.
+   */
+  public function testEntitySubclass() {
+    entity_test_create_bundle('bundle_class');
+
+    // Verify statically created entity with bundle class returns correct class.
+    $entity = EntityTestBundleClass::create();
+    $this->assertTrue($entity instanceof EntityTestBundleClass);
+
+    // Verify statically created entity with bundle class returns correct
+    // bundle.
+    $entity = EntityTestBundleClass::create(['type' => 'custom']);
+    $this->assertTrue($entity instanceof EntityTestBundleClass);
+    $this->assertEquals('bundle_class', $entity->bundle());
+
+    // Verify that the entity storage creates the entity using the proper class.
+    $entity = $this->storage->create(['type' => 'bundle_class']);
+    $this->assertTrue($entity instanceof EntityTestBundleClass);
+
+    // 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 EntityTestBundleClass);
+  }
+
+  /**
+   * Checks exception is thrown if multiple classes implement the same bundle.
+   */
+  public function testAmbiguousBundleClassException() {
+    $this->container->get('state')->set('entity_test_bundle_class_enable_ambiguous_entity_types', TRUE);
+    $this->entityTypeManager->clearCachedDefinitions();
+    $this->expectException(AmbiguousBundleClassException::class);
+    entity_test_create_bundle('bundle_class');
+
+    // Since we now have two entity types that returns the same class for the
+    // same bundle, we expect this to throw an exception.
+    EntityTestBundleClass::create();
+  }
+
+  /**
+   * Checks exception thrown if a bundle class doesn't extend the entity class.
+   */
+  public function testBundleClassShouldExtendEntityClass() {
+    $this->container->get('state')->set('entity_test_bundle_class_non_inheriting', TRUE);
+    $this->entityTypeManager->clearCachedDefinitions();
+    $this->expectException(BundleClassInheritanceException::class);
+    entity_test_create_bundle('bundle_class');
+    $this->storage->create(['type' => 'bundle_class']);
+  }
+
+}
