diff --git a/core/core.services.yml b/core/core.services.yml
index 1ee07fe..4f556ca 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -478,7 +478,7 @@ services:
     class: Drupal\Core\Extension\ModuleInstaller
     tags:
       - { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator }
-    arguments: ['@app.root', '@module_handler', '@kernel', '@router.builder']
+    arguments: ['@app.root', '@module_handler', '@kernel', '@event_dispatcher']
     lazy: true
   content_uninstall_validator:
     class: Drupal\Core\Entity\ContentUninstallValidator
@@ -558,6 +558,11 @@ services:
   field_definition.listener:
     class: Drupal\Core\Field\FieldDefinitionListener
     arguments: ['@entity_type.manager', '@entity_field.manager', '@keyvalue', '@cache.discovery']
+  entity.module_install.subscriber:
+    class: \Drupal\Core\EventSubscriber\ModuleInstallerEntitySubscriber
+    arguments: ['@kernel']
+    tags:
+      - { name: event_subscriber }
   entity.form_builder:
     class: Drupal\Core\Entity\EntityFormBuilder
     arguments: ['@entity.manager', '@form_builder']
diff --git a/core/lib/Drupal/Core/EventSubscriber/ModuleInstallerEntitySubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ModuleInstallerEntitySubscriber.php
new file mode 100644
index 0000000..00b7b64
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/ModuleInstallerEntitySubscriber.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\DrupalKernelInterface;
+use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Extension\ModuleEvent;
+use Drupal\Core\Extension\ModuleEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Event subscriber for installing fields on module install / uninstall.
+ */
+class ModuleInstallerEntitySubscriber implements EventSubscriberInterface {
+
+  /**
+   * The application kernel to retrieve the rebuilt service container from.
+   *
+   * @var \Drupal\Core\DrupalKernelInterface
+   */
+  protected $kernel;
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param \Drupal\Core\DrupalKernelInterface
+   *   The application kernel to retrieve the rebuilt service container from.
+   */
+  public function __construct(DrupalKernelInterface $kernel) {
+    $this->kernel = $kernel;
+  }
+
+  /**
+   * Gets the entity definition update manager.
+   *
+   * The manager is retrieved dynamically, because the container is rebuilt
+   * after module installation.
+   *
+   * @return \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
+   */
+  protected function getEntityDefinitionUpdateManager() {
+    return $this->kernel->getContainer()->get('entity.definition_update_manager');
+  }
+
+  /**
+   * Gets the entity definition update manager.
+   *
+   * The manager is retrieved dynamically, because the container is rebuilt
+   * after module installation.
+   *
+   * @return \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected function getEntityFieldManager() {
+    return $this->kernel->getContainer()->get('entity_field.manager');
+  }
+
+  /**
+   * Gets the entity type manager.
+   *
+   * The manager is retrieved dynamically, because the container is rebuilt
+   * after module installation.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected function getEntityTypeManager() {
+    return $this->kernel->getContainer()->get('entity_type.manager');
+  }
+
+  /**
+   * Installs the required entity schema.
+   *
+   * @param \Drupal\Core\Extension\ModuleEvent $event
+   *   The module event.
+   */
+  public function onModuleInstall(ModuleEvent $event) {
+    $module = $event->getModule();
+    // Notify interested components that this module's entity types and
+    // field storage definitions are new. For example, a SQL-based storage
+    // handler can use this as an opportunity to create the necessary
+    // database tables.
+    foreach ($this->getEntityTypeManager()->getDefinitions() as $entity_type) {
+      if ($entity_type->getProvider() == $module) {
+        $this->getEntityDefinitionUpdateManager()->installEntityType($entity_type);
+      }
+      elseif ($entity_type->isSubclassOf(FieldableEntityInterface::CLASS)) {
+        // The module being installed may be adding new fields to existing
+        // entity types. Field definitions for any entity type defined by
+        // the module are handled in the if branch.
+        foreach ($this->getEntityFieldManager()->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
+          if ($storage_definition->getProvider() == $module) {
+            // If the module being installed is also defining a storage key
+            // for the entity type, the entity schema may not exist yet. It
+            // will be created later in that case.
+            try {
+              $this->getEntityDefinitionUpdateManager()->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition);
+            }
+            catch (EntityStorageException $e) {
+              watchdog_exception('system', $e, 'An error occurred while notifying the creation of the @name field storage definition: "!message" in %function (line %line of %file).', ['@name' => $storage_definition->getName()]);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Uninstalls the required entity schema.
+   *
+   * @param \Drupal\Core\Extension\ModuleEvent $event
+   *   The module event.
+   */
+  public function onModuleUninstall(ModuleEvent $event) {
+    $module = $event->getModule();
+    // Notify interested components that this module's entity types are being
+    // deleted. For example, a SQL-based storage handler can use this as an
+    // opportunity to drop the corresponding database tables.
+    foreach ($this->getEntityTypeManager()->getDefinitions() as $entity_type) {
+      if ($entity_type->getProvider() == $module) {
+        $this->getEntityDefinitionUpdateManager()->uninstallEntityType($entity_type);
+      }
+      elseif ($entity_type->isSubclassOf(FieldableEntityInterface::CLASS)) {
+        // The module being uninstalled may be removing fields from existing
+        // entity types. Field definitions for any entity type defined by
+        // the module are handled in the if branch.
+        $entity_type_id = $entity_type->id();
+        /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
+        $storage = $this->getEntityTypeManager()->getStorage($entity_type_id);
+        foreach ($this->getEntityFieldManager()->getFieldStorageDefinitions($entity_type_id) as $storage_definition) {
+          // @todo We need to trigger field purging here.
+          //   See https://www.drupal.org/node/2282119.
+          if ($storage_definition->getProvider() == $module && !$storage->countFieldData($storage_definition, TRUE)) {
+            $this->getEntityDefinitionUpdateManager()->uninstallFieldStorageDefinition($storage_definition);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $info[ModuleEvents::MODULE_INSTALLED] = 'onModuleInstall';
+    $info[ModuleEvents::MODULE_UNINSTALLED] = 'onModuleUninstall';
+    return $info;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ModuleEvent.php b/core/lib/Drupal/Core/Extension/ModuleEvent.php
new file mode 100644
index 0000000..472f46c
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/ModuleEvent.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\Core\Extension;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Describes a module-related event.
+ */
+class ModuleEvent extends Event {
+
+  /**
+   * The module's machine name.
+   *
+   * @var string
+   */
+  protected $module;
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param string $module
+   *   The module's machine name.
+   */
+  public function __construct($module) {
+    $this->module = $module;
+  }
+
+
+  /**
+   * Returns the module's machine name.
+   *
+   * @return string
+   */
+  public function getModule() {
+    return $this->module;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ModuleEvents.php b/core/lib/Drupal/Core/Extension/ModuleEvents.php
new file mode 100644
index 0000000..e95fb7d
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/ModuleEvents.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Extension\ModuleEvents.
+ */
+
+namespace Drupal\Core\Extension;
+
+final class ModuleEvents {
+
+  /**
+   * This event is fired when modules are installed.
+   */
+  const MODULE_INSTALLED = 'core.modules.installed';
+
+  /**
+   * This event is fired when modules are uninstalled.
+   */
+  const MODULE_UNINSTALLED = 'core.modules.unstalled';
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index a903b8a..9f545e4 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -8,6 +8,7 @@
 use Drupal\Core\DrupalKernelInterface;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\FieldableEntityInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
 /**
  * Default implementation of the module installer.
@@ -46,6 +47,13 @@ class ModuleInstaller implements ModuleInstallerInterface {
   protected $uninstallValidators;
 
   /**
+   * The event dispatcher.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
    * Constructs a new ModuleInstaller instance.
    *
    * @param string $root
@@ -54,14 +62,17 @@ class ModuleInstaller implements ModuleInstallerInterface {
    *   The module handler.
    * @param \Drupal\Core\DrupalKernelInterface $kernel
    *   The drupal kernel.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
    *
    * @see \Drupal\Core\DrupalKernel
    * @see \Drupal\Core\CoreServiceProvider
    */
-  public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel) {
+  public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel, EventDispatcherInterface $event_dispatcher) {
     $this->root = $root;
     $this->moduleHandler = $module_handler;
     $this->kernel = $kernel;
+    $this->eventDispatcher = $event_dispatcher;
   }
 
   /**
@@ -207,36 +218,8 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
           $version = max(max($versions), $version);
         }
 
-        // Notify interested components that this module's entity types and
-        // field storage definitions are new. For example, a SQL-based storage
-        // handler can use this as an opportunity to create the necessary
-        // database tables.
-        // @todo Clean this up in https://www.drupal.org/node/2350111.
-        $entity_manager = \Drupal::entityManager();
-        $update_manager = \Drupal::entityDefinitionUpdateManager();
-        foreach ($entity_manager->getDefinitions() as $entity_type) {
-          if ($entity_type->getProvider() == $module) {
-            $update_manager->installEntityType($entity_type);
-          }
-          elseif ($entity_type->isSubclassOf(FieldableEntityInterface::CLASS)) {
-            // The module being installed may be adding new fields to existing
-            // entity types. Field definitions for any entity type defined by
-            // the module are handled in the if branch.
-            foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
-              if ($storage_definition->getProvider() == $module) {
-                // If the module being installed is also defining a storage key
-                // for the entity type, the entity schema may not exist yet. It
-                // will be created later in that case.
-                try {
-                  $update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition);
-                }
-                catch (EntityStorageException $e) {
-                  watchdog_exception('system', $e, 'An error occurred while notifying the creation of the @name field storage definition: "!message" in %function (line %line of %file).', ['@name' => $storage_definition->getName()]);
-                }
-              }
-            }
-          }
-        }
+        $event = new ModuleEvent($module);
+        $this->eventDispatcher->dispatch(ModuleEvents::MODULE_INSTALLED, $event);
 
         // Install default configuration of the module.
         $config_installer = \Drupal::service('config.installer');
@@ -394,31 +377,8 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
       \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider'));
       \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder'));
 
-      // Notify interested components that this module's entity types are being
-      // deleted. For example, a SQL-based storage handler can use this as an
-      // opportunity to drop the corresponding database tables.
-      // @todo Clean this up in https://www.drupal.org/node/2350111.
-      $update_manager = \Drupal::entityDefinitionUpdateManager();
-      foreach ($entity_manager->getDefinitions() as $entity_type) {
-        if ($entity_type->getProvider() == $module) {
-          $update_manager->uninstallEntityType($entity_type);
-        }
-        elseif ($entity_type->isSubclassOf(FieldableEntityInterface::CLASS)) {
-          // The module being installed may be adding new fields to existing
-          // entity types. Field definitions for any entity type defined by
-          // the module are handled in the if branch.
-          $entity_type_id = $entity_type->id();
-          /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
-          $storage = $entity_manager->getStorage($entity_type_id);
-          foreach ($entity_manager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) {
-            // @todo We need to trigger field purging here.
-            //   See https://www.drupal.org/node/2282119.
-            if ($storage_definition->getProvider() == $module && !$storage->countFieldData($storage_definition, TRUE)) {
-              $update_manager->uninstallFieldStorageDefinition($storage_definition);
-            }
-          }
-        }
-      }
+      $event = new ModuleEvent($module);
+      $this->eventDispatcher->dispatch(ModuleEvents::MODULE_UNINSTALLED, $event);
 
       // Remove the schema.
       drupal_uninstall_schema($module);
