diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index 3564f1f..86f2575 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -480,8 +480,7 @@ public function getPropertyDefinition($name) {
    */
   public function getPropertyDefinitions() {
     if (!isset($this->fieldDefinitions)) {
-      $bundle = $this->bundle != $this->entityType ? $this->bundle : NULL;
-      $this->fieldDefinitions = \Drupal::entityManager()->getFieldDefinitions($this->entityType, $bundle);
+      $this->fieldDefinitions = \Drupal::entityManager()->getFieldDefinitions($this->entityType, $this->bundle);
     }
     return $this->fieldDefinitions;
   }
@@ -984,4 +983,10 @@ public function referencedEntities() {
     return $referenced_entities;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitionsByBundle(array &$field_definitions, $entity_type, $bundle) {
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
index 862b23f..0173b1e 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
@@ -62,6 +62,23 @@ public function initTranslation($langcode);
   public static function baseFieldDefinitions($entity_type);
 
   /**
+   * Alter base field definitions for the given bundle.
+   *
+   * @todo Document and/or provide a better DX for field overrides.
+   *
+   * @param string $entity_type
+   *   The entity type to return properties for. Useful when a single class is
+   *   used for multiple, possibly dynamic entity types.
+   * @param string $bundle
+   *   Name of the bundle for which the overrides should be applied.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions
+   *   List of base fields for the given entity type.
+   *
+   * @see \Drupal\Core\Entity\EntityManagerInterface::getFieldDefinitions()
+   */
+  public static function baseFieldDefinitionsByBundle(array &$field_definitions, $entity_type, $bundle);
+
+  /**
    * Returns whether the entity has a field with the given name.
    *
    * @param string $field_name
diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php
index 83a671c..6e0a56f 100644
--- a/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -75,22 +75,27 @@ class EntityManager extends PluginManagerBase implements EntityManagerInterface
   protected $languageManager;
 
   /**
-   * An array of field information per entity type, i.e. containing definitions.
+   * Static cache of base field definitions.
    *
    * @var array
-   *
-   * @see hook_entity_field_info()
    */
-  protected $entityFieldInfo;
+  protected $baseFieldDefinitions;
 
   /**
-   * Static cache of field definitions per bundle and entity type.
+   * Static cache of field definitions excluding per-bundle fields.
    *
    * @var array
    */
   protected $fieldDefinitions;
 
   /**
+   * Static cache of field definitions per bundle and entity type.
+   *
+   * @var array
+   */
+  protected $fieldDefinitionsByBundle;
+
+  /**
    * The root paths.
    *
    * @see self::__construct().
@@ -327,90 +332,112 @@ public function getAdminRouteInfo($entity_type, $bundle) {
    * {@inheritdoc}
    */
   public function getFieldDefinitions($entity_type, $bundle = NULL) {
-    if (!isset($this->entityFieldInfo[$entity_type])) {
-      // First, try to load from cache.
-      $cid = 'entity_field_definitions:' . $entity_type . ':' . $this->languageManager->getLanguage(Language::TYPE_INTERFACE)->id;
-      if ($cache = $this->cache->get($cid)) {
-        $this->entityFieldInfo[$entity_type] = $cache->data;
-      }
-      else {
-        // @todo: Refactor to allow for per-bundle overrides.
-        // See https://drupal.org/node/2114707.
-        $class = $this->factory->getPluginClass($entity_type, $this->getDefinition($entity_type));
-
-        $this->entityFieldInfo[$entity_type] = array(
-          'definitions' => $class::baseFieldDefinitions($entity_type),
-          // Contains definitions of optional (per-bundle) fields.
-          'optional' => array(),
-          // An array keyed by bundle name containing the optional fields added
-          // by the bundle.
-          'bundle map' => array(),
-        );
-
-        // Invoke hooks.
-        $result = $this->moduleHandler->invokeAll($entity_type . '_field_info');
-        $this->entityFieldInfo[$entity_type] = NestedArray::mergeDeep($this->entityFieldInfo[$entity_type], $result);
-        $result = $this->moduleHandler->invokeAll('entity_field_info', array($entity_type));
-        $this->entityFieldInfo[$entity_type] = NestedArray::mergeDeep($this->entityFieldInfo[$entity_type], $result);
-
-        // Automatically set the field name for non-configurable fields.
-        foreach (array('definitions', 'optional') as $key) {
-          foreach ($this->entityFieldInfo[$entity_type][$key] as $field_name => &$definition) {
-            if ($definition instanceof FieldDefinition) {
-              $definition->setName($field_name);
-            }
-          }
-        }
-
-        // Invoke alter hooks.
-        $hooks = array('entity_field_info', $entity_type . '_field_info');
-        $this->moduleHandler->alter($hooks, $this->entityFieldInfo[$entity_type], $entity_type);
-
-        // Ensure all basic fields are not defined as translatable.
-        $entity_info = $this->getDefinition($entity_type);
-        $keys = array_intersect_key(array_filter($entity_info['entity_keys']), array_flip(array('id', 'revision', 'uuid', 'bundle')));
-        $untranslatable_fields = array_flip(array('langcode') + $keys);
-        foreach (array('definitions', 'optional') as $key) {
-          foreach ($this->entityFieldInfo[$entity_type][$key] as $field_name => &$definition) {
-            if (isset($untranslatable_fields[$field_name]) && $definition->isTranslatable()) {
-              throw new \LogicException(format_string('The @field field cannot be translatable.', array('@field' => $definition->getLabel())));
-            }
-          }
-        }
-
-        $this->cache->set($cid, $this->entityFieldInfo[$entity_type], CacheBackendInterface::CACHE_PERMANENT, array('entity_info' => TRUE, 'entity_field_info' => TRUE));
-      }
-    }
-
+    // If there's no bundle specified, only return fields that are not bundle
+    // specific.
     if (!$bundle) {
-      return $this->entityFieldInfo[$entity_type]['definitions'];
-    }
-    else {
-      // Add in per-bundle fields.
-      if (!isset($this->fieldDefinitions[$entity_type][$bundle])) {
-        $this->fieldDefinitions[$entity_type][$bundle] = $this->entityFieldInfo[$entity_type]['definitions'];
-        if (isset($this->entityFieldInfo[$entity_type]['bundle map'][$bundle])) {
-          $this->fieldDefinitions[$entity_type][$bundle] += array_intersect_key($this->entityFieldInfo[$entity_type]['optional'], array_flip($this->entityFieldInfo[$entity_type]['bundle map'][$bundle]));
+      // Check the static cache.
+      if (!isset($this->fieldDefinitions[$entity_type])) {
+        // Not prepared, try to load from cache.
+        $cid = 'entity_field_definitions:' . $entity_type . ':' . $this->languageManager->getLanguage(Language::TYPE_INTERFACE)->id;
+        if ($cache = $this->cache->get($cid)) {
+          $this->fieldDefinitions[$entity_type] = $cache->data;
+        }
+        else {
+          // Rebuild the definitions and put it into the cache.
+          $this->fieldDefinitions[$entity_type] = $this->buildFieldDefinitions($entity_type, $bundle);
+          $this->cache->set($cid, $this->fieldDefinitions[$entity_type], CacheBackendInterface::CACHE_PERMANENT, array('entity_info' => TRUE, 'entity_field_info' => TRUE));
         }
       }
-      return $this->fieldDefinitions[$entity_type][$bundle];
+      return $this->fieldDefinitions[$entity_type];
+    }
+
+    // We have a bundle, so get the field definitions for that specific bundle.
+    if (!isset($this->fieldDefinitionsByBundle[$entity_type][$bundle])) {
+      // Not prepared, try to load from cache.
+      $cid = 'entity_field_definitions_bundle:' . $entity_type . ':' . $bundle . ':' . $this->languageManager->getLanguage(Language::TYPE_INTERFACE)->id;
+      if ($cache = $this->cache->get($cid)) {
+        $this->fieldDefinitionsByBundle[$entity_type][$bundle] = $cache->data;
+      }
+      else {
+        // Rebuild the definitions and put it into the cache.
+        $this->fieldDefinitionsByBundle[$entity_type][$bundle] = $this->buildFieldDefinitions($entity_type, $bundle);
+        $this->cache->set($cid, $this->fieldDefinitionsByBundle[$entity_type][$bundle], CacheBackendInterface::CACHE_PERMANENT, array('entity_info' => TRUE, 'entity_field_info' => TRUE));
+      }
     }
+    return $this->fieldDefinitionsByBundle[$entity_type][$bundle];
   }
 
   /**
    * {@inheritdoc}
    */
   public function getFieldDefinitionsByConstraints($entity_type, array $constraints) {
-    // @todo: Add support for specifying multiple bundles.
     return $this->getFieldDefinitions($entity_type, isset($constraints['Bundle']) ? $constraints['Bundle'] : NULL);
   }
 
   /**
+   * Builds entity field definitions.
+   *
+   * @param string $entity_type
+   *   The entity type to get field definitions for. Only entity types that
+   *   implement \Drupal\Core\Entity\ContentEntityInterface are supported.
+   * @param string|null $bundle
+   *   (optional) The entity bundle for which to get field definitions. If NULL
+   *   is passed, no bundle-specific fields are included. Defaults to NULL.
+   *
+   * @return \Drupal\Core\Field\FieldDefinitionInterface[]
+   *   An array of entity field definitions, keyed by field name.
+   */
+  protected function buildFieldDefinitions($entity_type, $bundle) {
+    $class = $this->factory->getPluginClass($entity_type, $this->getDefinition($entity_type));
+
+    // Build the base fields only once for all bundles.
+    if (empty($this->baseFieldDefinitions[$entity_type])) {
+      $this->baseFieldDefinitions[$entity_type] = $class::baseFieldDefinitions($entity_type);
+    }
+    $field_definitions = $this->baseFieldDefinitions[$entity_type];
+
+    // When we build fields for a specific bundle, allow the entity class to
+    // override the base fields.
+    if ($bundle) {
+      $class::baseFieldDefinitionsByBundle($field_definitions, $entity_type, $bundle);
+    }
+
+    // Invoke hooks.
+    $result = $this->moduleHandler->invokeAll($entity_type . '_field_info', array($bundle));
+    $field_definitions = NestedArray::mergeDeep($field_definitions, $result);
+    $result = $this->moduleHandler->invokeAll('entity_field_info', array($entity_type, $bundle));
+    $field_definitions = NestedArray::mergeDeep($field_definitions, $result);
+
+    // Automatically set the field name for non-configurable fields.
+    foreach ($field_definitions as $field_name => $definition) {
+      if ($definition instanceof FieldDefinition) {
+        $definition->setName($field_name);
+      }
+    }
+
+    // Invoke alter hooks.
+    $hooks = array('entity_field_info', $entity_type . '_field_info');
+    $this->moduleHandler->alter($hooks, $field_definitions, $entity_type, $bundle);
+
+    // Ensure all basic fields are not defined as translatable.
+    $entity_info = $this->getDefinition($entity_type);
+    $keys = array_intersect_key(array_filter($entity_info['entity_keys']), array_flip(array('id', 'revision', 'uuid', 'bundle')));
+    $untranslatable_fields = array_flip(array('langcode') + $keys);
+    foreach ($field_definitions as $field_name => $definition) {
+      if (isset($untranslatable_fields[$field_name]) && $definition->isTranslatable()) {
+        throw new \LogicException(format_string('The @field field cannot be translatable.', array('@field' => $definition->getLabel())));
+      }
+    }
+    return $field_definitions;
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function clearCachedFieldDefinitions() {
-    unset($this->entityFieldInfo);
-    unset($this->fieldDefinitions);
+    $this->baseFieldDefinitions = array();
+    $this->fieldDefinitions = array();
+    $this->fieldDefinitionsByBundle = array();
     $this->cache->deleteTags(array('entity_field_info' => TRUE));
   }
 
diff --git a/core/lib/Drupal/Core/Field/ConfigFieldItemList.php b/core/lib/Drupal/Core/Field/ConfigFieldItemList.php
index 0da28d0..33b1137 100644
--- a/core/lib/Drupal/Core/Field/ConfigFieldItemList.php
+++ b/core/lib/Drupal/Core/Field/ConfigFieldItemList.php
@@ -7,56 +7,12 @@
 
 namespace Drupal\Core\Field;
 
-use Drupal\Core\Field\Plugin\DataType\FieldInstanceInterface;
-use Drupal\Core\TypedData\TypedDataInterface;
-use Drupal\field\Field;
-
 /**
  * Represents a configurable entity field item list.
  */
 class ConfigFieldItemList extends FieldItemList implements ConfigFieldItemListInterface {
 
   /**
-   * The Field instance definition.
-   *
-   * @var \Drupal\field\Entity\FieldInstance
-   */
-  protected $instance;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct($definition, $name = NULL, TypedDataInterface $parent = NULL) {
-    parent::__construct($definition, $name, $parent);
-    // Definition can be the field config or field instance.
-    if ($definition instanceof FieldInstanceInterface) {
-      $this->instance = $definition;
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFieldDefinition() {
-    // Configurable fields have the field_config entity injected as definition,
-    // but we want to return the more specific field instance here.
-    // @todo: Overhaul this once we have per-bundle field definitions injected,
-    // see https://drupal.org/node/2114707.
-    if (!isset($this->instance)) {
-      $entity = $this->getEntity();
-      $instances = Field::fieldInfo()->getBundleInstances($entity->entityType(), $entity->bundle());
-      if (isset($instances[$this->getName()])) {
-        $this->instance = $instances[$this->getName()];
-      }
-      else {
-        // For base fields, fall back to return the general definition.
-        return parent::getFieldDefinition();
-      }
-    }
-    return $this->instance;
-  }
-
-  /**
    * {@inheritdoc}
    */
   public function getConstraints() {
diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
index f2bd59f..b9c4a47 100644
--- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php
+++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
@@ -368,4 +368,14 @@ public function getConstraints(DataDefinitionInterface $definition) {
 
     return $constraints;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearCachedDefinitions() {
+    parent::clearCachedDefinitions();
+    $this->prototypes = array();
+  }
+
+
 }
diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module
index 0d476cb..9dab1ea 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -114,25 +114,22 @@ function content_translation_entity_bundle_info_alter(&$bundles) {
 /**
  * Implements hook_entity_field_info_alter().
  */
-function content_translation_entity_field_info_alter(&$info, $entity_type) {
+function content_translation_entity_field_info_alter(&$fields, $entity_type, $bundle) {
   $translation_settings = config('content_translation.settings')->get($entity_type);
 
   if ($translation_settings) {
     // Currently field translatability is defined per-field but we may want to
     // make it per-instance instead, so leaving the possibility open for further
     // easier refactoring.
-    $fields = array();
+    $field_settings = array();
     foreach ($translation_settings as $bundle => $settings) {
-      $fields += !empty($settings['content_translation']['fields']) ? $settings['content_translation']['fields'] : array();
+      $field_settings += !empty($settings['content_translation']['fields']) ? $settings['content_translation']['fields'] : array();
     }
 
-    $keys = array('definitions', 'optional');
     foreach ($fields as $name => $translatable) {
-      foreach ($keys as $key) {
-        if (isset($info[$key][$name])) {
-          $info[$key][$name]->setTranslatable((bool) $translatable);
-          break;
-        }
+      if (isset($field_settings[$name])) {
+        $fields[$name]->setTranslatable((bool) $translatable);
+        break;
       }
     }
   }
diff --git a/core/modules/entity/lib/Drupal/entity/EntityDisplayBase.php b/core/modules/entity/lib/Drupal/entity/EntityDisplayBase.php
index 310520c..d7c79af 100644
--- a/core/modules/entity/lib/Drupal/entity/EntityDisplayBase.php
+++ b/core/modules/entity/lib/Drupal/entity/EntityDisplayBase.php
@@ -46,17 +46,6 @@
   public $bundle;
 
   /**
-   * A partial entity, created via _field_create_entity_from_ids() from
-   * $targetEntityType and $bundle.
-   *
-   * @var \Drupal\Core\Entity\EntityInterface
-   *
-   * @todo Remove when getFieldDefinition() is fixed to not need it.
-   *   https://drupal.org/node/2114707
-   */
-  private $targetEntity;
-
-  /**
    * View or form mode to be displayed.
    *
    * @var string
@@ -321,14 +310,7 @@ public function getHighestWeight() {
    * Returns the field definition of a field.
    */
   protected function getFieldDefinition($field_name) {
-    // @todo Replace this entire implementation with
-    //   \Drupal::entityManager()->getFieldDefinition() when it can hand the
-    //   $instance objects - https://drupal.org/node/2114707
-    if (!isset($this->targetEntity)) {
-      $this->targetEntity = _field_create_entity_from_ids((object) array('entity_type' => $this->targetEntityType, 'bundle' => $this->bundle, 'entity_id' => NULL));
-    }
-    if (($this->targetEntity instanceof ContentEntityInterface) && $this->targetEntity->hasField($field_name)) {
-      return $this->targetEntity->get($field_name)->getFieldDefinition();
-    }
+    $definitions = \Drupal::entityManager()->getFieldDefinitions($this->targetEntityType, $this->bundle);
+    return isset($definitions[$field_name]) ? $definitions[$field_name] : NULL;
   }
 }
diff --git a/core/modules/field/field.info.inc b/core/modules/field/field.info.inc
index 5b1e81a..a1af0ca 100644
--- a/core/modules/field/field.info.inc
+++ b/core/modules/field/field.info.inc
@@ -39,6 +39,7 @@ function field_info_cache_clear() {
   \Drupal::typedData()->clearCachedDefinitions();
   \Drupal::service('plugin.manager.field.field_type')->clearCachedDefinitions();
   \Drupal::service('config.factory')->reset();
+  \Drupal::typedData()->clearCachedDefinitions();
 
   Field::fieldInfo()->flush();
 }
diff --git a/core/modules/field/field.module b/core/modules/field/field.module
index e19408c..ae691c8 100644
--- a/core/modules/field/field.module
+++ b/core/modules/field/field.module
@@ -7,6 +7,7 @@
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Template\Attribute;
+use Drupal\field\Field;
 use Drupal\field\FieldInterface;
 use Drupal\field\FieldInstanceInterface;
 
@@ -188,27 +189,11 @@ function field_system_info_alter(&$info, $file, $type) {
 /**
  * Implements hook_entity_field_info() to define all configured fields.
  */
-function field_entity_field_info($entity_type) {
-  $property_info = array();
-
-  foreach (field_info_instances($entity_type) as $bundle_name => $instances) {
-    $optional = $bundle_name != $entity_type;
-    // @todo: Improve hook_entity_field_info() to allow per-bundle field
-    // definitions, such that we can pass on field instances as field
-    // definitions here. See https://drupal.org/node/2114707.
-
-    foreach ($instances as $field_name => $instance) {
-      if ($optional) {
-        $property_info['optional'][$field_name] = $instance->getField();
-        $property_info['bundle map'][$bundle_name][] = $field_name;
-      }
-      else {
-        $property_info['definitions'][$field_name] = $instance->getField();
-      }
-    }
+function field_entity_field_info($entity_type, $bundle) {
+  // Configurable fields are always for a specific bundle.
+  if ($bundle) {
+    return Field::fieldInfo()->getBundleInstances($entity_type, $bundle);
   }
-
-  return $property_info;
 }
 
 /**
diff --git a/core/modules/node/lib/Drupal/node/Entity/Node.php b/core/modules/node/lib/Drupal/node/Entity/Node.php
index fee4c18..6d44101 100644
--- a/core/modules/node/lib/Drupal/node/Entity/Node.php
+++ b/core/modules/node/lib/Drupal/node/Entity/Node.php
@@ -375,7 +375,6 @@ public static function baseFieldDefinitions($entity_type) {
     $fields['title'] = FieldDefinition::create('text')
       ->setLabel(t('Title'))
       ->setDescription(t('The title of this node, always treated as non-markup plain text.'))
-      ->setClass('\Drupal\node\NodeTitleItemList')
       ->setRequired(TRUE)
       ->setTranslatable(TRUE)
       ->setSettings(array(
@@ -434,4 +433,15 @@ public static function baseFieldDefinitions($entity_type) {
     return $fields;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitionsByBundle(array &$field_definitions, $entity_type, $bundle) {
+    $node_type = node_type_load($bundle);
+    if (isset($node_type->title_label)) {
+      $field_definitions['title'] = clone $field_definitions['title'];
+      $field_definitions['title']->setLabel($node_type->title_label);
+    }
+  }
+
 }
diff --git a/core/modules/node/lib/Drupal/node/NodeTitleItemList.php b/core/modules/node/lib/Drupal/node/NodeTitleItemList.php
deleted file mode 100644
index 2b81193..0000000
--- a/core/modules/node/lib/Drupal/node/NodeTitleItemList.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\node\NodeTitleItemList.
- */
-
-namespace Drupal\node;
-
-use Drupal\Core\Field\FieldDefinition;
-use Drupal\Core\Field\FieldItemList;
-
-/**
- * @todo This class is a temporary hack for allowing the label of the node title
- *   field to vary by node type. Remove it when https://drupal.org/node/2114707
- *   is solved.
- */
-class NodeTitleItemList extends FieldItemList {
-
-  /**
-   * {@inheritdoc}
-   *
-   * The typehint for $definition is a class rather than an interface, because
-   * there is no interface for setLabel().
-   */
-  public function __construct(FieldDefinition $definition, $name, NodeInterface $node) {
-    $node_type = node_type_load($node->getType());
-    if (isset($node_type->title_label)) {
-      $definition->setLabel($node_type->title_label);
-    }
-    parent::__construct($definition, $name, $node);
-  }
-
-}
diff --git a/core/modules/node/tests/modules/node_access_test/node_access_test.module b/core/modules/node/tests/modules/node_access_test/node_access_test.module
index bcfc3aa..832658f 100644
--- a/core/modules/node/tests/modules/node_access_test/node_access_test.module
+++ b/core/modules/node/tests/modules/node_access_test/node_access_test.module
@@ -81,13 +81,13 @@ function node_access_test_permission() {
 /**
  * Implements hook_entity_field_info().
  */
-function node_access_test_entity_field_info($entity_type) {
+function node_access_test_entity_field_info($entity_type, $bundle) {
   if ($entity_type === 'node') {
-      $info['definitions']['private'] = FieldDefinition::create('boolean')
+      $fields['private'] = FieldDefinition::create('boolean')
         ->setLabel(t('Private'))
         ->setComputed(TRUE);
 
-    return $info;
+    return $fields;
   }
 }
 
diff --git a/core/modules/path/path.module b/core/modules/path/path.module
index 8e96610..3cdd5de 100644
--- a/core/modules/path/path.module
+++ b/core/modules/path/path.module
@@ -208,11 +208,11 @@ function path_form_taxonomy_term_form_alter(&$form, $form_state) {
  */
 function path_entity_field_info($entity_type) {
   if ($entity_type === 'taxonomy_term' || $entity_type === 'node') {
-    $info['definitions']['path'] = FieldDefinition::create('path')
+    $fields['path'] = FieldDefinition::create('path')
       ->setLabel(t('The path alias'))
       ->setComputed(TRUE);
 
-    return $info;
+    return $fields;
   }
 }
 
diff --git a/core/modules/system/entity.api.php b/core/modules/system/entity.api.php
index 5b880cd..0b28f20 100644
--- a/core/modules/system/entity.api.php
+++ b/core/modules/system/entity.api.php
@@ -633,58 +633,55 @@ function hook_entity_form_display_alter(\Drupal\Core\Entity\Display\EntityFormDi
  *
  * @param string $entity_type
  *   The entity type for which to define entity fields.
+ * @param string|null $bundle
+ *   The name of the bundle to return field definitions for. If NULL, only
+ *   fields that are not bundle specific must be returned.
  *
- * @return array
- *   An array of entity field information having the following optional entries:
- *   - definitions: An array of field definitions to add to all entities of this
- *     type, keyed by field name.
- *   - optional: An array of field definitions for optional entity fields, keyed
- *     by field name. Optional fields are fields that only exist for certain
- *     bundles of the entity type.
- *   - bundle map: An array keyed by bundle name, containing the names of
- *     optional fields that entities of this bundle have.
+ * @return \Drupal\Core\Field\FieldDefinitionInterface[]
+ *   An array of field definitions.
  *
  * @see hook_entity_field_info_alter()
  * @see \Drupal\Core\Field\FieldDefinitionInterface
  * @see \Drupal\Core\Entity\EntityManagerInterface::getFieldDefinitions()
  * @see \Drupal\Core\TypedData\TypedDataManager::create()
  */
-function hook_entity_field_info($entity_type) {
-  if (mymodule_uses_entity_type($entity_type)) {
-    $info = array();
-    $info['definitions']['mymodule_text'] = FieldDefinition::create('string')
+function hook_entity_field_info($entity_type, $bundle) {
+  if ($bundle && mymodule_uses_entity_type($entity_type, $bundle)) {
+    $fields = array();
+    $fields['mymodule_text'] = FieldDefinition::create('string')
       ->setLabel(t('The text'))
       ->setDescription(t('A text property added by mymodule.'))
       ->setComputed(TRUE)
       ->setClass('\Drupal\mymodule\EntityComputedText');
 
-    if ($entity_type == 'node') {
-      // Add a property only to entities of the 'article' bundle.
-      $info['optional']['mymodule_text_more'] = FieldDefinition::create('string')
+    // Add a property only to nodes of the 'article' bundle.
+    if ($entity_type == 'node' && $bundle == 'article') {
+      $fields['mymodule_text_more'] = FieldDefinition::create('string')
         ->setLabel(t('More text'))
         ->setComputed(TRUE)
         ->setClass('\Drupal\mymodule\EntityComputedMoreText');
-
-      $info['bundle map']['article'][0] = 'mymodule_text_more';
     }
-    return $info;
+    return $fields;
   }
 }
 
 /**
  * Alter defined entity fields.
  *
- * @param array $info
- *   The entity field info array as returned by hook_entity_field_info().
+ * @param \Drupal\Core\Field\FieldDefinitionInterface[] $fields
+ *   An array of field definitions.
  * @param string $entity_type
  *   The entity type for which entity fields are defined.
+ * @param string|null $bundle
+ *   The name of the bundle to return field definitions for. If NULL, only
+ *   information that is not bundle specific must be provided.
  *
  * @see hook_entity_field_info()
  */
-function hook_entity_field_info_alter(&$info, $entity_type) {
-  if (!empty($info['definitions']['mymodule_text'])) {
+function hook_entity_field_info_alter(&$fields, $entity_type, $bundle) {
+  if ($bundle && mymodule_uses_entity_type($entity_type, $bundle) && !empty($fields['mymodule_text'])) {
     // Alter the mymodule_text field to use a custom class.
-    $info['definitions']['mymodule_text']->setClass('\Drupal\anothermodule\EntityComputedText');
+    $fields['mymodule_text']->setClass('\Drupal\anothermodule\EntityComputedText');
   }
 }
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php
index 1e777a6..128efed 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php
@@ -355,7 +355,7 @@ public function testIntrospection() {
    */
   protected function checkIntrospection($entity_type) {
     // Test getting metadata upfront.
-    $definitions = \Drupal::entityManager()->getFieldDefinitions($entity_type);
+    $definitions = \Drupal::entityManager()->getFieldDefinitions($entity_type, $entity_type);
     $this->assertEqual($definitions['name']->getType(), 'string', $entity_type .': Name field found.');
     $this->assertEqual($definitions['user_id']->getType(), 'entity_reference', $entity_type .': User field found.');
     $this->assertEqual($definitions['field_test_text']->getType(), 'text', $entity_type .': Test-text-field field found.');
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index d16c503..fc64515 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -72,10 +72,10 @@ function entity_test_entity_info_alter(&$info) {
 /**
  * Implements hook_entity_field_info_alter().
  */
-function entity_test_entity_field_info_alter(&$info, $entity_type) {
+function entity_test_entity_field_info_alter(&$fields, $entity_type, $bundle) {
   if ($entity_type == 'entity_test_mulrev' && ($names = \Drupal::state()->get('entity_test.field_definitions.translatable'))) {
     foreach ($names as $name => $value) {
-      $info['definitions'][$name]->setTranslatable($value);
+      $fields[$name]->setTranslatable($value);
     }
   }
 }
