diff --git a/core/core.services.yml b/core/core.services.yml
index c42ca2d..0a82937 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -594,6 +594,11 @@ services:
     arguments: ['@entity_type.manager']
     tags:
       - { name: event_subscriber }
+  entity_events_subscriber:
+    class: Drupal\Core\EventSubscriber\EntityEventsSubscriber
+    arguments: ['@module_handler']
+    tags:
+      - { name: event_subscriber }
   entity.definition_update_manager:
     class: Drupal\Core\Entity\EntityDefinitionUpdateManager
     arguments: ['@entity_type.manager', '@entity.last_installed_schema.repository', '@entity_field.manager', '@entity_type.listener', '@field_storage_definition.listener']
diff --git a/core/lib/Drupal/Core/Entity/EntityEvent.php b/core/lib/Drupal/Core/Entity/EntityEvent.php
new file mode 100644
index 0000000..9c52f07
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/EntityEvent.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\Core\Entity;
+
+use Drupal\Component\EventDispatcher\Event;
+
+/**
+ * Represents various entity events.
+ *
+ * @see \Drupal\Core\Entity\EntityEvents
+ * @see \Drupal\Core\Entity\EntityStorageBase::invokeHook()
+ */
+class EntityEvent extends Event {
+
+  /**
+   * The entity object.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface
+   */
+  protected $entity;
+
+  /**
+   * EntityEvent constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity object.
+   */
+  public function __construct(EntityInterface $entity) {
+    $this->entity = $entity;
+  }
+
+  /**
+   * Returns the entity wrapped by this event.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The entity object.
+   */
+  public function getEntity() {
+    return $this->entity;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/EntityEvents.php b/core/lib/Drupal/Core/Entity/EntityEvents.php
new file mode 100644
index 0000000..94972df
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/EntityEvents.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace Drupal\Core\Entity;
+
+/**
+ * Defines events for entity CRUD.
+ *
+ * @see \Drupal\Core\Entity\EntityEvent
+ * @see \Drupal\Core\Entity\EntityStorageBase::invokeHook()
+ */
+final class EntityEvents {
+
+  /**
+   * Maps entity hook names to event names.
+   *
+   * @var string[]
+   */
+  public static $hookToEventMap = [
+    'create' => self::CREATE,
+    'presave' => self::PRESAVE,
+    'insert' => self::INSERT,
+    'update' => self::UPDATE,
+    'predelete' => self::PREDELETE,
+    'delete' => self::DELETE,
+  ];
+
+  /**
+   * Name of the event fired when creating an entity.
+   *
+   * This hook runs after a new entity object has just been instantiated.
+   *
+   * @see hook_entity_create()
+   *
+   * @Event
+   *
+   * @var string
+   */
+  const CREATE = 'entity.create';
+
+  /**
+   * Name of the event fired before an entity is created or updated.
+   *
+   * You can get the original entity object from $entity->original when it is an
+   * update of the entity.
+   *
+   * @see hook_entity_presave()
+   *
+   * @Event
+   *
+   * @var string
+   */
+  const PRESAVE = 'entity.presave';
+
+  /**
+   * Name of the event fired after creating an entity.
+   *
+   * This event fires once the entity has been stored. Note that changes to the
+   * entity made by subscribers to the event will not be saved.
+   *
+   * @see hook_entity_insert()
+   *
+   * @Event
+   *
+   * @var string
+   */
+  const INSERT = 'entity.insert';
+
+  /**
+   * Name of the event fired after updating an existing entity.
+   *
+   * This event fires once the entity has been stored. Note that changes to the
+   * entity made by subscribers to the event will not be saved. Get the original
+   * entity object from $entity->original.
+   *
+   * @see hook_entity_update()
+   *
+   * @Event
+   *
+   * @var string
+   */
+  const UPDATE = 'entity.update';
+
+  /**
+   * Name of the event fired before deleting an entity.
+   *
+   * @see hook_entity_predelete()
+   *
+   * @Event
+   *
+   * @var string
+   */
+  const PREDELETE = 'entity.predelete';
+
+  /**
+   * Name of the event fired after deleting an entity.
+   *
+   * @see hook_entity_delete()
+   *
+   * @Event
+   *
+   * @var string
+   */
+  const DELETE = 'entity.delete';
+
+  /**
+   * Returns the event name for creation of an entity of a specific type.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   *
+   * @return string
+   *   The event name.
+   */
+  public static function create($entity_type_id) {
+    return self::CREATE . '.' . $entity_type_id;
+  }
+
+  /**
+   * Returns the event name when presaving an entity of a specific type.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   *
+   * @return string
+   *   The event name.
+   */
+  public static function presave($entity_type_id) {
+    return self::PRESAVE . '.' . $entity_type_id;
+  }
+
+  /**
+   * Returns the event name for inserting an entity of a specific type.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   *
+   * @return string
+   *   The event name.
+   */
+  public static function insert($entity_type_id) {
+    return self::INSERT . '.' . $entity_type_id;
+  }
+
+  /**
+   * Returns the event name for updating an entity of a specific type.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   *
+   * @return string
+   *   The event name.
+   */
+  public static function update($entity_type_id) {
+    return self::UPDATE . '.' . $entity_type_id;
+  }
+
+  /**
+   * Returns the event name just before deleting an entity of a specific type.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   *
+   * @return string
+   *   The event name.
+   */
+  public static function predelete($entity_type_id) {
+    return self::PREDELETE . '.' . $entity_type_id;
+  }
+
+  /**
+   * Returns the event name for deletion of an entity of a specific type.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   *
+   * @return string
+   *   The event name.
+   */
+  public static function delete($entity_type_id) {
+    return self::DELETE . '.' . $entity_type_id;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
index 8b78be1..c3894c0 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
@@ -197,8 +197,23 @@ protected function setStaticCache(array $entities) {
   protected function invokeHook($hook, EntityInterface $entity) {
     // Invoke the hook.
     $this->moduleHandler()->invokeAll($this->entityTypeId . '_' . $hook, [$entity]);
-    // Invoke the respective entity-level hook.
-    $this->moduleHandler()->invokeAll('entity_' . $hook, [$entity]);
+
+    // Dispatch events for the invoked hook.
+    /** @var \Drupal\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher */
+    $event_dispatcher = \Drupal::service('event_dispatcher');
+    $event = new EntityEvent($entity);
+
+    // Dispatch an event for the entity type-specific hook (i.e.,
+    // hook_ENTITY_TYPE_$hook).
+    if (method_exists(EntityEvents::class, $hook)) {
+      $event_name = call_user_func([EntityEvents::class, $hook], $this->entityTypeId);
+      $event_dispatcher->dispatch($event, $event_name);
+    }
+
+    // Dispatch an event for the entity-level hook (i.e., hook_entity_$hook).
+    if (isset(EntityEvents::$hookToEventMap[$hook])) {
+      $event_dispatcher->dispatch($event, EntityEvents::$hookToEventMap[$hook]);
+    }
   }
 
   /**
diff --git a/core/lib/Drupal/Core/EventSubscriber/EntityEventsSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/EntityEventsSubscriber.php
new file mode 100644
index 0000000..74d02ff
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/EntityEventsSubscriber.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Entity\EntityEvent;
+use Drupal\Core\Entity\EntityEvents;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Executes entity hooks.
+ */
+class EntityEventsSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Constructs a new EntityRouteProviderSubscriber instance.
+   *
+   * @todo Inject entity type handler to subscribe to entity type level hooks.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(ModuleHandlerInterface $module_handler) {
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * Provides hooks for entity specific events.
+   *
+   * @param \Drupal\Core\Entity\EntityEvent $event
+   *   The entity event.
+   * @param string $event_name
+   *   The related entity event name.
+   */
+  public function onEntityEvent(EntityEvent $event, $event_name) {
+    $hook = array_search($event_name, EntityEvents::$hookToEventMap);
+    if ($hook) {
+      $this->moduleHandler->invokeAll('entity_' . $hook, [$event->getEntity()]);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Hooks should be executed before other subscribers for BC.
+    $priority = -1000;
+    $events[EntityEvents::CREATE][] = ['onEntityEvent', $priority];
+    $events[EntityEvents::PRESAVE][] = ['onEntityEvent', $priority];
+    $events[EntityEvents::INSERT][] = ['onEntityEvent', $priority];
+    $events[EntityEvents::UPDATE][] = ['onEntityEvent', $priority];
+    $events[EntityEvents::PREDELETE][] = ['onEntityEvent', $priority];
+    $events[EntityEvents::DELETE][] = ['onEntityEvent', $priority];
+    return $events;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test_event/entity_test_event.info.yml b/core/modules/system/tests/modules/entity_test_event/entity_test_event.info.yml
new file mode 100644
index 0000000..9a0bfb0
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_event/entity_test_event.info.yml
@@ -0,0 +1,5 @@
+name: 'Entity test event'
+type: module
+description: 'Provides entity events.'
+package: Testing
+version: VERSION
diff --git a/core/modules/system/tests/modules/entity_test_event/entity_test_event.services.yml b/core/modules/system/tests/modules/entity_test_event/entity_test_event.services.yml
new file mode 100644
index 0000000..e20270c
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_event/entity_test_event.services.yml
@@ -0,0 +1,5 @@
+services:
+  entity_test_event.event_subscriber:
+    class: \Drupal\entity_test_event\EventSubscriber\TestEventSubscriber
+    tags:
+      - { name: event_subscriber }
diff --git a/core/modules/system/tests/modules/entity_test_event/src/EventSubscriber/TestEventSubscriber.php b/core/modules/system/tests/modules/entity_test_event/src/EventSubscriber/TestEventSubscriber.php
new file mode 100644
index 0000000..812c89c
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_event/src/EventSubscriber/TestEventSubscriber.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\entity_test_event\EventSubscriber;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Drupal\Core\Entity\EntityEvents;
+use Drupal\Core\Entity\EntityEvent;
+
+/**
+ * Defines the test event subscriber class.
+ */
+class TestEventSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Test EntityEvents::INSERT event callback.
+   *
+   * @param \Drupal\Core\Entity\EntityEvent $event
+   *   Insert event.
+   */
+  public function onInsert(EntityEvent $event) {
+    if ($event->getEntity()->name->value === 'hei') {
+      $event->getEntity()->name->value .= ' ho';
+    }
+  }
+
+  /**
+   * Test EntityEvents::UPDATE event callback.
+   *
+   * @param \Drupal\Core\Entity\EntityEvent $event
+   *   Insert event.
+   */
+  public function onUpdate(EntityEvent $event) {
+    if ($event->getEntity()->name->value === 'hei') {
+      $event->getEntity()->name->value .= ' ho';
+    }
+  }
+
+  /**
+   * Test EntityEvents::DELETE event callback.
+   *
+   * @param \Drupal\Core\Entity\EntityEvent $event
+   *   Insert event.
+   */
+  public function onDelete(EntityEvent $event) {
+    if ($event->getEntity()->name->value === 'hei ho') {
+      EntityTest::create([
+        'name' => 'hei_ho',
+      ])->save();
+      $event->getEntity()->name->value .= ' ho';
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[EntityEvents::INSERT] = 'onInsert';
+    $events[EntityEvents::UPDATE] = 'onUpdate';
+    $events[EntityEvents::DELETE] = 'onDelete';
+    return $events;
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityEventsTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityEventsTest.php
new file mode 100644
index 0000000..a2f5f57
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityEventsTest.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @group Entity
+ */
+class EntityEventsTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['entity_test', 'entity_test_event', 'user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('entity_test');
+  }
+
+  /**
+   * Test insert event.
+   *
+   * @see \Drupal\entity_test_event\EventSubscriber\TestEventSubscriber
+   */
+  public function testEventsInsert() {
+    $entity = EntityTest::create([
+      'name' => 'hei',
+    ]);
+    $entity->save();
+    $this->assertEquals('hei ho', $entity->name->value);
+  }
+
+  /**
+   * Test update event.
+   *
+   * @see \Drupal\entity_test_event\EventSubscriber\TestEventSubscriber
+   */
+  public function testEventsUpdate() {
+    $entity = EntityTest::create([
+      'name' => 'meh',
+    ]);
+    $entity->save();
+    $this->assertEquals('meh', $entity->name->value);
+
+    $entity->name->value = 'hei';
+    $entity->save();
+    $this->assertEquals('hei ho', $entity->name->value);
+  }
+
+  /**
+   * Test delete event.
+   *
+   * @see \Drupal\entity_test_event\EventSubscriber\TestEventSubscriber
+   */
+  public function testEventsDelete() {
+    $entities = \Drupal::entityTypeManager()->getStorage('entity_test')
+      ->loadByProperties(['name' => 'hei_ho']);
+    $this->assertCount(0, $entities);
+
+    $entity = EntityTest::create([
+      'name' => 'hei',
+    ]);
+    $entity->save();
+    $entity->delete();
+
+    // Note the delete event creates another entity.
+    $entities = \Drupal::entityTypeManager()->getStorage('entity_test')
+      ->loadByProperties(['name' => 'hei_ho']);
+    $this->assertCount(1, $entities);
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php
index ead39ac..49c1c4d 100644
--- a/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Language\Language;
 use Drupal\Tests\UnitTestCase;
 use Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
 /**
  * @coversDefaultClass \Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage
@@ -133,6 +134,7 @@ protected function setUpKeyValueEntityStorage($uuid_key = 'uuid') {
     $this->languageManager->expects($this->any())
       ->method('getCurrentLanguage')
       ->will($this->returnValue($language));
+    $event_dispatcher = $this->prophesize(EventDispatcherInterface::class);
 
     $this->entityStorage = new KeyValueEntityStorage($this->entityType, $this->keyValueStore, $this->uuidService, $this->languageManager, new MemoryCache());
     $this->entityStorage->setModuleHandler($this->moduleHandler);
@@ -140,6 +142,7 @@ protected function setUpKeyValueEntityStorage($uuid_key = 'uuid') {
     $container = new ContainerBuilder();
     $container->set('entity_field.manager', $this->entityFieldManager);
     $container->set('entity_type.manager', $this->entityTypeManager);
+    $container->set('event_dispatcher', $event_dispatcher->reveal());
     $container->set('language_manager', $this->languageManager);
     $container->set('cache_tags.invalidator', $this->cacheTagsInvalidator);
     \Drupal::setContainer($container);
