diff --git a/core/includes/update.inc b/core/includes/update.inc
index 70edeaf..f0020ee 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -219,6 +219,58 @@ function update_do_one($module, $number, $dependency_map, &$context) {
 }
 
 /**
+ * Executes a single hook_update_after_$name().
+ *
+ * @param string $function
+ *   The function name, that should be executed.
+ * @param array $context
+ *   The batch context array.
+ */
+function update_after_do_one($function, &$context) {
+  $ret = [];
+  list($module, ) = explode('_update_after_', $function);
+  module_load_include('inc', $module, $module . '.update_after');
+  if (function_exists($function)) {
+    try {
+      $ret['results']['query'] = $function($context['sandbox']);
+      $ret['results']['success'] = TRUE;
+
+      $key_value_store = \Drupal::keyValue('update_after');
+      $executed_updates = $key_value_store->get('core.update__update_after', []);
+      $executed_updates[] = $function;
+      $key_value_store->set('core.update__update_after', $executed_updates);
+    }
+      // @TODO We may want to do different error handling for different
+      // exception types, but for now we'll just log the exception and
+      // return the message for printing.
+    catch (Exception $e) {
+      watchdog_exception('update', $e);
+
+      $variables = Error::decodeException($e);
+      unset($variables['backtrace']);
+      $ret['#abort'] = [
+        'success' => FALSE,
+        'query' => t('%type: @message in %function (line %line of %file).', $variables)
+      ];
+    }
+  }
+
+  if (isset($context['sandbox']['#finished'])) {
+    $context['finished'] = $context['sandbox']['#finished'];
+    unset($context['sandbox']['#finished']);
+  }
+
+  $context['results'][$function] = $ret;
+
+  if (!empty($ret['#abort'])) {
+    // Record this function in the list of updates that were aborted.
+    $context['results']['#abort'][] = $function;
+  }
+
+  $context['message'] = 'Updating content after: ' . Html::escape($function);
+}
+
+/**
  * Returns a list of all the pending database updates.
  *
  * @return
diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
index 3238ce6..5ce0f7b 100644
--- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
+++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
@@ -95,10 +95,18 @@ class ExtensionDiscovery {
    *
    * @param string $root
    *   The app root.
+   * @param bool $use_file_cache
+   *   Whether file cache should be used.
+   * @param string[] $profile_directories
+   *   The available profile directories
+   * @param string $site_path
+   *   The path to the site.
    */
-  public function __construct($root) {
+  public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL) {
     $this->root = $root;
-    $this->fileCache = FileCacheFactory::get('extension_discovery');
+    $this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL;
+    $this->profileDirectories = $profile_directories;
+    $this->sitePath = $site_path;
   }
 
   /**
@@ -172,7 +180,7 @@ public function scan($type, $include_tests = NULL) {
       $searchdirs[static::ORIGIN_SITE] = \Drupal::service('site.path');
     }
     else {
-      $searchdirs[static::ORIGIN_SITE] = DrupalKernel::findSitePath(Request::createFromGlobals());
+      $searchdirs[static::ORIGIN_SITE] = isset($this->sitePath) ? $this->sitePath : DrupalKernel::findSitePath(Request::createFromGlobals());
     }
 
     // Unless an explicit value has been passed, manually check whether we are
@@ -427,7 +435,7 @@ protected function scanDirectory($dir, $include_tests) {
         continue;
       }
 
-      if ($cached_extension = $this->fileCache->get($fileinfo->getPathName())) {
+      if ($this->fileCache && $cached_extension = $this->fileCache->get($fileinfo->getPathName())) {
         $files[$cached_extension->getType()][$key] = $cached_extension;
         continue;
       }
@@ -467,7 +475,10 @@ protected function scanDirectory($dir, $include_tests) {
       $extension->origin = $dir;
 
       $files[$type][$key] = $extension;
-      $this->fileCache->set($fileinfo->getPathName(), $extension);
+
+      if ($this->fileCache) {
+        $this->fileCache->set($fileinfo->getPathName(), $extension);
+      }
     }
     return $files;
   }
diff --git a/core/lib/Drupal/Core/Update/UpdateRegistry.php b/core/lib/Drupal/Core/Update/UpdateRegistry.php
new file mode 100644
index 0000000..58ec918
--- /dev/null
+++ b/core/lib/Drupal/Core/Update/UpdateRegistry.php
@@ -0,0 +1,169 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Update\UpdateRegistry.
+ */
+
+namespace Drupal\Core\Update;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+
+/**
+ * Provides all and missing hook_update_$updateName_X implementations.
+ *
+ * It therefore scans for functions in $module.$updateName.inc named like
+ * $module_update_$updateName_X with X being a machine name.
+ */
+class UpdateRegistry {
+
+  protected $updateName = 'update_after';
+
+  /**
+   * The app root.
+   *
+   * @var string
+   */
+  protected $root;
+
+  /**
+   * The filename of the log file.
+   *
+   * @var string
+   */
+  protected $logFilename;
+
+  /**
+   * @var string[]
+   */
+  protected $enabledModules;
+
+  /**
+   * The key value storage.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   */
+  protected $keyValue;
+
+  /**
+   * Should we respect update functions in tests.
+   *
+   * @var bool|null
+   */
+  protected $includeTests = NULL;
+
+  public function __construct($root, $site_path, array $enabled_modules, KeyValueStoreInterface $key_value, $include_tests = NULL) {
+    $this->root = $root;
+    $this->sitePath = $site_path;
+    $this->enabledModules = $enabled_modules;
+    $this->keyValue = $key_value;
+    $this->includeTests = $include_tests;
+  }
+
+  /**
+   * Gets all available update functions.
+   *
+   * @return callable[]
+   */
+  protected function getAvailableUpdateFunctions() {
+    $regexp = '/^(?<module>.+)_' . $this->updateName . '_(?<version>.+)$/';
+    $functions = get_defined_functions();
+
+    $updates = [];
+    foreach (preg_grep('/_' . $this->updateName . '_/', $functions['user']) as $function) {
+      // If this function is a module update function, add it to the list of
+      // module updates.
+      if (preg_match($regexp, $function, $matches)) {
+        $updates[] = $matches['module'] . '_' . $this->updateName . '_' . $matches['version'];
+      }
+    }
+
+    return $updates;
+  }
+
+  /**
+   * Find all update functions that haven't been executed
+   *
+   * @return callable[]
+   */
+  public function getMissingUpdateFunctions() {
+    // We need a) the list of active modules (we get that from the config
+    // bootstrap factory) and b) the path to the modules, we use the extension
+    // discovery for that.
+
+    // Scan the module list.
+    $extension_discovery = new ExtensionDiscovery($this->root, FALSE, [], $this->sitePath);
+    $module_extensions = $extension_discovery->scan('module');
+
+    $profile_extensions = $extension_discovery->scan('profile');
+    $extensions = array_merge($module_extensions, $profile_extensions);
+
+    $this->loadUpdateFiles($extensions);
+
+    // First figure out which hook_update got executed already.
+    $existing_update_functions = $this->keyValue->get('core.update__' . $this->updateName, []);
+
+    $available_update_functions = $this->getAvailableUpdateFunctions();
+    $not_executed_update_functions = array_diff($available_update_functions, $existing_update_functions);
+
+    return $not_executed_update_functions;
+  }
+
+  protected function loadUpdateFiles(array $module_extensions) {
+    // Load all the {$this->updateName}.inc files.
+    foreach ($this->enabledModules as $module) {
+      if (isset($module_extensions[$module])) {
+        $this->loadUpdateFile($module_extensions[$module]);
+      }
+    }
+  }
+
+  /**
+   * Loads the {$this->updateName}.inc file for a given extension.
+   *
+   * @param \Drupal\Core\Extension\Extension $module
+   */
+  protected function loadUpdateFile(Extension $module) {
+    $filename = $this->root . '/' . $module->getPath() . '/' . $module->getName() . ".{$this->updateName}.inc";
+    if (file_exists($filename)) {
+      include_once $filename;
+    }
+  }
+
+  /**
+   * Returns a list of all the pending database updates.
+   *
+   * @return array[]
+   *   An associative array keyed by module name which contains all information
+   *   about database updates that need to be run, and any updates that are not
+   *   going to proceed due to missing requirements. The system module will
+   *   always be listed first.
+   *
+   *   The subarray for each module can contain the following keys:
+   *   - start: The starting update that is to be processed. If this does not
+   *       exist then do not process any updates for this module as there are
+   *       other requirements that need to be resolved.
+   *   - pending: An array of all the pending updates for the module including
+   *       the update number and the description from source code comment for
+   *       each update function. This array is keyed by the update number.
+   */
+  public function getMissingUpdateList() {
+    $functions = $this->getMissingUpdateFunctions();
+
+    $ret = [];
+    foreach ($functions as $function) {
+      list($module, $update) = explode("_{$this->updateName}_", $function);
+      // The description for an update comes from its Doxygen.
+      $func = new \ReflectionFunction($function);
+      $description = trim(str_replace(array("\n", '*', '/'), '', $func->getDocComment()), ' ');
+      $ret[$module]['pending'][$update] = "$update - $description";
+      if (!isset($ret[$module]['start'])) {
+        $ret[$module]['start'] = $update;
+      }
+    }
+    return $ret;
+  }
+
+}
diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php
index fa8104d..5b44880 100644
--- a/core/modules/system/src/Controller/DbUpdateController.php
+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -15,6 +15,7 @@
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\State\StateInterface;
+use Drupal\Core\Update\UpdateRegistry;
 use Drupal\Core\Url;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Response;
@@ -257,35 +258,45 @@ protected function selection(Request $request) {
     // Ensure system.module's updates appear first.
     $build['start']['system'] = array();
 
-    $updates = update_get_update_list();
     $starting_updates = array();
-    $incompatible_updates_exist = FALSE;
-    foreach ($updates as $module => $update) {
-      if (!isset($update['start'])) {
-        $build['start'][$module] = array(
-          '#type' => 'item',
-          '#title' => $module . ' module',
-          '#markup'  => $update['warning'],
-          '#prefix' => '<div class="messages messages--warning">',
-          '#suffix' => '</div>',
-        );
-        $incompatible_updates_exist = TRUE;
-        continue;
-      }
-      if (!empty($update['pending'])) {
-        $starting_updates[$module] = $update['start'];
-        $build['start'][$module] = array(
-          '#type' => 'hidden',
-          '#value' => $update['start'],
-        );
-        $build['start'][$module . '_updates'] = array(
-          '#theme' => 'item_list',
-          '#items' => $update['pending'],
-          '#title' => $module . ' module',
-        );
+    foreach (['update', 'update_after'] as $update_type) {
+      switch ($update_type) {
+        case 'update':
+          $updates = update_get_update_list();
+          break;
+        case 'update_after':
+          $after_update_registry = $this->getUpdateAfterRegistry();
+          $updates = $after_update_registry->getMissingUpdateList();
+          break;
       }
-      if (isset($update['pending'])) {
-        $count = $count + count($update['pending']);
+      $incompatible_updates_exist = FALSE;
+      foreach ($updates as $module => $update) {
+        if (!isset($update['start'])) {
+          $build['start'][$module] = array(
+            '#type' => 'item',
+            '#title' => $module . ' module',
+            '#markup' => $update['warning'],
+            '#prefix' => '<div class="messages messages--warning">',
+            '#suffix' => '</div>',
+          );
+          $incompatible_updates_exist = TRUE;
+          continue;
+        }
+        if (!empty($update['pending'])) {
+          $starting_updates[$module] = $update['start'];
+          $build['start'][$module] = array(
+            '#type' => 'hidden',
+            '#value' => $update['start'],
+          );
+          $build['start'][$module . '_updates'] = array(
+            '#theme' => 'item_list',
+            '#items' => $update['pending'],
+            '#title' => $module . ' module',
+          );
+        }
+        if (isset($update['pending'])) {
+          $count = $count + count($update['pending']);
+        }
       }
     }
 
@@ -575,6 +586,19 @@ protected function triggerBatch(Request $request) {
       }
     }
 
+    $after_update_registry = $this->getUpdateAfterRegistry();
+    $after_updates = $after_update_registry->getMissingUpdateFunctions();
+
+    if ($after_updates) {
+      // Now we rebuild all caches and after that execute the hook_update_after()
+      // functions.
+      $operations[] = ['drupal_flush_all_caches', []];
+      foreach ($after_updates as $function) {
+        $operations[] = ['update_after_do_one', [$function]];
+      }
+    }
+
+
     $batch['operations'] = $operations;
     $batch += array(
       'title' => $this->t('Updating'),
@@ -589,6 +613,15 @@ protected function triggerBatch(Request $request) {
   }
 
   /**
+   * Gets the update registry instance for the hook_update_after updates.
+   *
+   * @return \Drupal\Core\Update\UpdateRegistry
+   */
+  protected function getUpdateAfterRegistry() {
+    return new UpdateRegistry($this->root, \Drupal::service('site.path'), array_keys($this->moduleHandler()->getModuleList()), \Drupal::keyValue('update_after'));
+  }
+
+  /**
    * Finishes the update process and stores the results for eventual display.
    *
    * After the updates run, all caches are flushed. The update results are
diff --git a/core/modules/system/src/Tests/Update/UpdateAfterTest.php b/core/modules/system/src/Tests/Update/UpdateAfterTest.php
new file mode 100644
index 0000000..0b85fcb
--- /dev/null
+++ b/core/modules/system/src/Tests/Update/UpdateAfterTest.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Update\UpdateAfterTest.
+ */
+
+namespace Drupal\system\Tests\Update;
+
+/**
+ * Tests hook_update_after().
+ *
+ * @group Update
+ */
+class UpdateAfterTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz',
+      __DIR__ . '/../../../tests/fixtures/update/drupal-8.update-test-after-enabled.php',
+    ];
+  }
+
+  public function testUpdateAfter() {
+    $this->runUpdates();
+
+    $this->assertIdentical([
+      'update_test_after_update_after_first',
+      'update_test_after_update_after_second',
+      'update_test_after_update_after_test1',
+      'update_test_after_update_after_test0',
+    ], \Drupal::state()->get('update_test_after_execution', []));
+  }
+
+}
diff --git a/core/modules/system/tests/fixtures/update/drupal-8.update-test-after-enabled.php b/core/modules/system/tests/fixtures/update/drupal-8.update-test-after-enabled.php
new file mode 100644
index 0000000..681a5fb
--- /dev/null
+++ b/core/modules/system/tests/fixtures/update/drupal-8.update-test-after-enabled.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Partial database to mimic the installation of the update_test_after module.
+ */
+
+use Drupal\Core\Database\Database;
+
+$connection = Database::getConnection();
+
+// Set the schema version.
+$connection->merge('key_value')
+  ->condition('collection', 'system.schema')
+  ->condition('name', 'update_test_schema')
+  ->fields([
+    'collection' => 'system.schema',
+    'name' => 'update_test_after',
+    'value' => 'i:8000;',
+  ])
+  ->execute();
+
+// Update core.extension.
+$extensions = $connection->select('config')
+  ->fields('config', ['data'])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute()
+  ->fetchField();
+$extensions = unserialize($extensions);
+$extensions['module']['update_test_after'] = 8000;
+$connection->update('config')
+  ->fields([
+    'data' => serialize($extensions),
+  ])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute();
+
diff --git a/core/modules/system/tests/modules/update_test_after/update_test_after.info.yml b/core/modules/system/tests/modules/update_test_after/update_test_after.info.yml
new file mode 100644
index 0000000..9dcff3a
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_after/update_test_after.info.yml
@@ -0,0 +1,3 @@
+core: 8.x
+name: Update test after
+type: module
diff --git a/core/modules/system/tests/modules/update_test_after/update_test_after.update_after.inc b/core/modules/system/tests/modules/update_test_after/update_test_after.update_after.inc
new file mode 100644
index 0000000..b45197a
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_after/update_test_after.update_after.inc
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * First update.
+ */
+function update_test_after_update_after_first() {
+  $execution = \Drupal::state()->get('update_test_after_execution', []);
+  $execution[] = __FUNCTION__;
+  \Drupal::state()->set('update_test_after_execution', $execution);
+}
+
+/**
+ * Second update.
+ */
+function update_test_after_update_after_second() {
+  $execution = \Drupal::state()->get('update_test_after_execution', []);
+  $execution[] = __FUNCTION__;
+  \Drupal::state()->set('update_test_after_execution', $execution);
+}
+
+/**
+ * Test1 update.
+ */
+function update_test_after_update_after_test1() {
+  $execution = \Drupal::state()->get('update_test_after_execution', []);
+  $execution[] = __FUNCTION__;
+  \Drupal::state()->set('update_test_after_execution', $execution);
+}
+
+/**
+ * Test2 update.
+ */
+function update_test_after_update_after_test0() {
+  $execution = \Drupal::state()->get('update_test_after_execution', []);
+  $execution[] = __FUNCTION__;
+  \Drupal::state()->set('update_test_after_execution', $execution);
+}
diff --git a/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php b/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php
new file mode 100644
index 0000000..2206fc2
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php
@@ -0,0 +1,175 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Update\UpdateRegistryTest.
+ */
+
+namespace Drupal\Tests\Core\Update;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+use Drupal\Core\Update\UpdateRegistry;
+use Drupal\Tests\UnitTestCase;
+use org\bovigo\vfs\vfsStream;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Update\UpdateRegistry
+ * @group Update
+ *
+ * @runTestsInSeparateProcesses
+ */
+class UpdateRegistryTest extends UnitTestCase {
+
+  protected function setupBasicModules($directories) {
+    $info_a = <<<'EOS'
+type: module
+name: Module A
+core: 8.x
+EOS;
+
+    $info_b = <<<'EOS'
+type: module
+name: Module B
+core: 8.x
+EOS;
+
+    $module_a = <<<'EOS'
+<?php
+
+/**
+ * Module A update A.
+ */
+function module_a_update_after_a() {
+}
+
+/**
+ * Module A update B.
+ */
+function module_a_update_after_b() {
+}
+
+EOS;
+    $module_b = <<<'EOS'
+<?php
+
+/**
+ * Module B update A.
+ */
+function module_b_update_after_a() {
+}
+
+EOS;
+    vfsStream::setup('drupal');
+    vfsStream::create(NestedArray::mergeDeep($directories, [
+      'sites' => [
+        'default' => [
+          'modules' => [
+            'module_a' => [
+              'module_a.update_after.inc' => $module_a,
+              'module_a.info.yml' => $info_a
+            ],
+            'module_b' => [
+              'module_b.update_after.inc' => $module_b,
+              'module_b.info.yml' => $info_b
+            ],
+          ]
+        ]
+      ],
+    ]));
+  }
+
+  /**
+   * @covers ::getMissingUpdateFunctions
+   */
+  public function testGetMissingUpdateFunctionsWithNoExistingUpdates() {
+    $this->setupBasicModules([]);
+
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('core.update__update_after', [])->wilLReturn([]);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+
+    $this->assertEquals([
+      'module_a_update_after_a',
+      'module_a_update_after_b',
+      'module_b_update_after_a'
+    ], $update_registry->getMissingUpdateFunctions());
+  }
+
+  /**
+   * @covers ::getMissingUpdateFunctions
+   */
+  public function testGetMissingUpdateFunctionsWithExistingUpdates() {
+    $this->setupBasicModules([]);
+
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('core.update__update_after', [])->wilLReturn(['module_a_update_after_a']);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+
+    $this->assertEquals(array_values([
+      'module_a_update_after_b',
+      'module_b_update_after_a'
+    ]), array_values($update_registry->getMissingUpdateFunctions()));
+
+  }
+
+  /**
+   * @covers ::getMissingUpdateList
+   */
+  public function testGetMissingUpdateList() {
+    $this->setupBasicModules([]);
+
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('core.update__update_after', [])->wilLReturn([]);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+
+    $expected = [];
+    $expected['module_a']['pending']['a'] = 'a - Module A update A.';
+    $expected['module_a']['pending']['b'] = 'b - Module A update B.';
+    $expected['module_a']['start'] = 'a';
+    $expected['module_b']['pending']['a'] = 'a - Module B update A.';
+    $expected['module_b']['start'] = 'a';
+
+    $this->assertEquals($expected, $update_registry->getMissingUpdateList());
+  }
+
+  /**
+   * @covers ::getMissingUpdateList
+   */
+  public function testGetMissingUpdateListWithExistingUpdates() {
+    $this->setupBasicModules([]);
+
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('core.update__update_after', [])->wilLReturn(['module_a_update_after_a']);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+
+    $expected = [];
+    $expected['module_a']['pending']['b'] = 'b - Module A update B.';
+    $expected['module_a']['start'] = 'b';
+    $expected['module_b']['pending']['a'] = 'a - Module B update A.';
+    $expected['module_b']['start'] = 'a';
+
+    $this->assertEquals($expected, $update_registry->getMissingUpdateList());
+  }
+
+}
