diff --git a/core/lib/Drupal/Component/Discovery/DiscoverableInterface.php b/core/lib/Drupal/Component/Discovery/DiscoverableInterface.php
index c04556f..b31370c 100644
--- a/core/lib/Drupal/Component/Discovery/DiscoverableInterface.php
+++ b/core/lib/Drupal/Component/Discovery/DiscoverableInterface.php
@@ -17,6 +17,9 @@
    *
    * @return array
    *   An array of discovered data keyed by provider.
+   *
+   * @throws \Drupal\Component\Discovery\DiscoveryException
+   *   Exception thrown if there is a problem during discovery.
    */
   public function findAll();
 
diff --git a/core/lib/Drupal/Component/Discovery/DiscoveryException.php b/core/lib/Drupal/Component/Discovery/DiscoveryException.php
new file mode 100644
index 0000000..c545033
--- /dev/null
+++ b/core/lib/Drupal/Component/Discovery/DiscoveryException.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Discovery\DiscoveryException.
+ */
+
+namespace Drupal\Component\Discovery;
+
+/**
+ * Exception thrown during discovery if the data is invalid.
+ */
+class DiscoveryException extends \RuntimeException {
+}
diff --git a/core/lib/Drupal/Component/Discovery/YamlDirectoryDiscovery.php b/core/lib/Drupal/Component/Discovery/YamlDirectoryDiscovery.php
new file mode 100644
index 0000000..21dbe67
--- /dev/null
+++ b/core/lib/Drupal/Component/Discovery/YamlDirectoryDiscovery.php
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Discovery\YamlDirectoryDiscovery.
+ */
+
+namespace Drupal\Component\Discovery;
+
+use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
+use Drupal\Component\Serialization\Yaml;
+use Drupal\Component\FileCache\FileCacheFactory;
+
+/**
+ * Discovers multiple YAML files in a set of directories.
+ */
+class YamlDirectoryDiscovery implements DiscoverableInterface {
+
+  /**
+   * An array of directories to scan, keyed by the provider.
+   *
+   * The value can either be a string or an array of strings. The string values
+   * should be the path of a directory to scan.
+   *
+   * @var array
+   */
+  protected $directories = [];
+
+  /**
+   * The suffix the file cache key.
+   *
+   * @var string
+   */
+  protected $fileCacheKeySuffix;
+
+  /**
+   * The key which contains the value to key the data by.
+   *
+   * @var string
+   */
+  protected $key;
+
+  /**
+   * Constructs a YamlDirectoryDiscovery object.
+   *
+   * @param array $directories
+   *   An array of directories to scan, keyed by the provider.
+   * @param string $file_cache_key_suffix
+   *   The file cache key suffix. This should be unique for each type of
+   *   discovery.
+   * @param string $key
+   *   (optional) The key contained in the discovered data that identifies it.
+   *   Defaults to 'id'.
+   */
+  public function __construct(array $directories, $file_cache_key_suffix, $key = 'id') {
+    $this->directories = $directories;
+    $this->fileCacheKeySuffix = $file_cache_key_suffix;
+    $this->key = $key;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function findAll() {
+    $all = array();
+
+    $files = $this->findFiles();
+
+    $file_cache = FileCacheFactory::get('yaml_discovery:' . $this->fileCacheKeySuffix);
+
+    // Try to load from the file cache first.
+    foreach ($file_cache->getMultiple(array_keys($files)) as $file => $data) {
+      $all[$files[$file]][$this->getIdentifier($file, $data)] = $data;
+      unset($files[$file]);
+    }
+
+    // If there are files left that were not returned from the cache, load and
+    // parse them now. This list was flipped above and is keyed by filename.
+    if ($files) {
+      foreach ($files as $file => $provider) {
+        // If a file is empty or its contents are commented out, return an empty
+        // array instead of NULL for type consistency.
+        try {
+          $data = Yaml::decode(file_get_contents($file)) ?: [];
+        }
+        catch (InvalidDataTypeException $e) {
+          throw new DiscoveryException("The $file contains invalid YAML");
+        }
+        $all[$provider][$this->getIdentifier($file, $data)] = $data;
+        $file_cache->set($file, $data);
+      }
+    }
+
+    return $all;
+  }
+
+  /**
+   * Gets the identifier from the data.
+   *
+   * @param array $data
+   *   The data from the YAML file.
+   *
+   * @return string
+   *   The identifier from the data.
+   */
+  protected function getIdentifier($file, array $data) {
+    if (!isset($data[$this->key])) {
+      throw new DiscoveryException("The $file contains no data in the identifier key '{$this->key}'");
+    }
+    return $data[$this->key];
+  }
+
+  /**
+   * Returns an array of providers keyed by file path.
+   *
+   * @return array
+   *   An array of providers keyed by file path.
+   */
+  protected function findFiles() {
+    $files = array();
+    $pattern = '/\.yml$/i';
+
+    foreach ($this->directories as $provider => $directories) {
+      $directories = (array) $directories;
+      foreach ($directories as $directory) {
+        if (is_dir($directory)) {
+          foreach (scandir($directory) as $file) {
+            if ($file[0] !== '.' && preg_match($pattern, $file)) {
+              $files[$directory . '/' . $file] = $provider;
+            }
+          }
+        }
+      }
+    }
+    return $files;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Discovery/YamlDirectoryDiscoveryTest.php b/core/tests/Drupal/Tests/Component/Discovery/YamlDirectoryDiscoveryTest.php
new file mode 100644
index 0000000..993dd9e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Discovery/YamlDirectoryDiscoveryTest.php
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Component\Discovery\YamlDiscoveryTest.
+ */
+
+namespace Drupal\Tests\Component\Discovery;
+
+use Drupal\Component\Discovery\DiscoveryException;
+use Drupal\Component\Discovery\YamlDirectoryDiscovery;
+use Drupal\Tests\UnitTestCase;
+use org\bovigo\vfs\vfsStream;
+use org\bovigo\vfs\vfsStreamWrapper;
+use org\bovigo\vfs\vfsStreamDirectory;
+
+/**
+ * YamlDirectoryDiscoveryTest component unit tests.
+ *
+ * @coversDefaultClass \Drupal\Component\Discovery\YamlDirectoryDiscovery
+ *
+ * @group Discovery
+ */
+class YamlDirectoryDiscoveryTest extends UnitTestCase {
+
+  /**
+   * Tests YAML directory discovery.
+   *
+   * @covers ::findAll
+   */
+  public function testDiscovery() {
+    vfsStreamWrapper::register();
+    $root = new vfsStreamDirectory('modules');
+    vfsStreamWrapper::setRoot($root);
+    $url = vfsStream::url('modules');
+
+    mkdir($url . '/test_1');
+    mkdir($url . '/test_1/subdir1');
+    mkdir($url . '/test_1/subdir2');
+    file_put_contents($url . '/test_1/subdir1/item_1.test.yml', "id: item1\nname: 'test1 item 1'");
+    file_put_contents($url . '/test_1/subdir2/item_2.test.yml', "id: item2\nname: 'test1 item 2'");
+
+    mkdir($url . '/test_2');
+    mkdir($url . '/test_2/subdir1');
+    mkdir($url . '/test_2/subdir2');
+    file_put_contents($url . '/test_2/subdir1/item_3.test.yml', "id: item3\nname: 'test2 item 3'");
+
+    mkdir($url . '/test_3');
+
+    mkdir($url . '/test_4');
+    mkdir($url . '/test_4/subdir1');
+    file_put_contents($url . '/test_4/subdir1/item_4.test.yml', "id: item4\nname: 'test4 item 4'");
+    // Set up the directories to search.
+    $directories = [
+      // Multiple directories both with valid items.
+      'test_1' => [$url . '/test_1/subdir1', $url . '/test_1/subdir2'],
+      // The subdir2 directory is empty.
+      'test_2' => [$url . '/test_2/subdir1', $url . '/test_2/subdir2'],
+      // Directories that do not exist.
+      'test_3' => [$url . '/test_3/subdir1', $url . '/test_3/subdir2'],
+      // A single directory.
+      'test_4' => $url . '/test_4/subdir1',
+    ];
+
+    $discovery = new YamlDirectoryDiscovery($directories, 'test');
+    $data = $discovery->findAll();
+
+    $this->assertSame(['id' => 'item1', 'name' => 'test1 item 1'], $data['test_1']['item1']);
+    $this->assertSame(['id' => 'item2', 'name' => 'test1 item 2'], $data['test_1']['item2']);
+    $this->assertEquals(2, count($data['test_1']), 'test_1 provides 2 items');
+
+    $this->assertSame(['id' => 'item3', 'name' => 'test2 item 3'], $data['test_2']['item3']);
+    $this->assertEquals(1, count($data['test_2']), 'test_2 provides 1 item');
+
+    $this->assertTrue(empty($data['test_3']), 'test_3 provides 0 items');
+
+    $this->assertSame(['id' => 'item4', 'name' => 'test4 item 4'], $data['test_4']['item4']);
+    $this->assertEquals(1, count($data['test_2']), 'test_4 provides 1 item');
+  }
+
+  /**
+   * Tests YAML directory discovery with an alternate ID key.
+   *
+   * @covers ::findAll
+   */
+  public function testDiscoveryAlternateId() {
+    vfsStreamWrapper::register();
+    $root = new vfsStreamDirectory('modules');
+    vfsStreamWrapper::setRoot($root);
+    $url = vfsStream::url('modules');
+
+    mkdir($url . '/test_1');
+    file_put_contents($url . '/test_1/item_1.test.yml', "alt_id: item1\nid: ignored");
+
+    // Set up the directories to search.
+    $directories = ['test_1' => $url . '/test_1'];
+
+    $discovery = new YamlDirectoryDiscovery($directories, 'test', 'alt_id');
+    $data = $discovery->findAll();
+
+    $this->assertSame(['alt_id' => 'item1', 'id' => 'ignored'], $data['test_1']['item1']);
+    $this->assertEquals(1, count($data['test_1']), 'test_1 provides 1 item');
+  }
+
+  /**
+   * Tests YAML directory discovery with a missing ID key.
+   *
+   * @covers ::findAll
+   */
+  public function testDiscoveryNoIdException() {
+    $this->setExpectedException(DiscoveryException::class, 'The vfs://modules/test_1/item_1.test.yml contains no data in the identifier key \'id\'');
+    vfsStreamWrapper::register();
+    $root = new vfsStreamDirectory('modules');
+    vfsStreamWrapper::setRoot($root);
+    $url = vfsStream::url('modules');
+
+    mkdir($url . '/test_1');
+    file_put_contents($url . '/test_1/item_1.test.yml', "");
+
+    // Set up the directories to search.
+    $directories = ['test_1' => $url . '/test_1'];
+
+    $discovery = new YamlDirectoryDiscovery($directories, 'test');
+    $discovery->findAll();
+  }
+
+  /**
+   * Tests YAML directory discovery with invalid YAML.
+   *
+   * @covers ::findAll
+   */
+  public function testDiscoveryInvalidYamlException() {
+    $this->setExpectedException(DiscoveryException::class, 'The vfs://modules/test_1/item_1.test.yml contains invalid YAML');
+    vfsStreamWrapper::register();
+    $root = new vfsStreamDirectory('modules');
+    vfsStreamWrapper::setRoot($root);
+    $url = vfsStream::url('modules');
+
+    mkdir($url . '/test_1');
+    file_put_contents($url . '/test_1/item_1.test.yml', "id: invalid\nfoo : [bar}");
+
+    // Set up the directories to search.
+    $directories = ['test_1' => $url . '/test_1'];
+
+    $discovery = new YamlDirectoryDiscovery($directories, 'test');
+    $discovery->findAll();
+  }
+
+}
