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..8cf45a1
--- /dev/null
+++ b/core/lib/Drupal/Component/Discovery/YamlDirectoryDiscovery.php
@@ -0,0 +1,160 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Discovery\YamlDirectoryDiscovery.
+ */
+
+namespace Drupal\Component\Discovery;
+
+use Drupal\Component\FileSystem\RegexDirectoryIterator;
+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 {
+
+  /**
+   * Defines the key in the discovered data where the file path is stored.
+   */
+  const DISCOVERY_PATH = '_discovery_path';
+
+  /**
+   * 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", 0, $e);
+        }
+        $data[static::DISCOVERY_PATH] = $file;
+        $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() {
+    $file_list = [];
+    foreach ($this->directories as $provider => $directories) {
+      $directories = (array) $directories;
+      foreach ($directories as $directory) {
+        if (is_dir($directory)) {
+          /** @var \SplFileInfo $fileInfo */
+          foreach ($this->getDirectoryIterator($directory) as $fileInfo) {
+            $file_list[$fileInfo->getPathname()] = $provider;
+          }
+        }
+      }
+    }
+    return $file_list;
+  }
+
+  /**
+   * Gets an iterator to loop over the files in the provided directory.
+   *
+   * This method is exists so that it is easy to replace this functionality in
+   * a class that extends this one. For example, it could be used to make the
+   * scan recursive.
+   *
+   * @param $directory
+   *   The directory to scan.
+   *
+   * @return \Traversable
+   *   An \Traversable object or array where the values are \SplFileInfo
+   *   objects.
+   */
+  protected function getDirectoryIterator($directory) {
+    return new RegexDirectoryIterator($directory, '/\.yml$/i');
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Plugin/Discovery/YamlDirectoryDiscovery.php b/core/lib/Drupal/Core/Plugin/Discovery/YamlDirectoryDiscovery.php
new file mode 100644
index 0000000..244394e
--- /dev/null
+++ b/core/lib/Drupal/Core/Plugin/Discovery/YamlDirectoryDiscovery.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Plugin\Discovery\YamlDiscovery.
+ */
+
+namespace Drupal\Core\Plugin\Discovery;
+
+use Drupal\Component\Discovery\YamlDirectoryDiscovery as ComponentYamlDirectoryDiscovery;
+
+/**
+ * Allows directories that contain YAML files to define plugin definitions.
+ */
+class YamlDirectoryDiscovery extends YamlDiscovery {
+
+  /**
+   * 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') {
+    // Intentionally does not call parent constructor as this class uses a
+    // different YAML discovery.
+    $this->discovery = new ComponentYamlDirectoryDiscovery($directories, $file_cache_key_suffix, $key);
+  }
+
+}
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..0289e5f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Discovery/YamlDirectoryDiscoveryTest.php
@@ -0,0 +1,157 @@
+<?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;
+
+/**
+ * YamlDirectoryDiscoveryTest component unit tests.
+ *
+ * @coversDefaultClass \Drupal\Component\Discovery\YamlDirectoryDiscovery
+ *
+ * @group Discovery
+ */
+class YamlDirectoryDiscoveryTest extends UnitTestCase {
+
+  /**
+   * Tests YAML directory discovery.
+   *
+   * @covers ::findAll
+   */
+  public function testDiscovery() {
+    vfsStream::setup('modules', NULL, [
+      'test_1' => [
+        'subdir1' => [
+          'item_1.test.yml' => "id: item1\nname: 'test1 item 1'",
+        ],
+        'subdir2' => [
+          'item_2.test.yml' => "id: item2\nname: 'test1 item 2'",
+        ],
+      ],
+      'test_2' => [
+        'subdir1' => [
+          'item_3.test.yml' => "id: item3\nname: 'test2 item 3'",
+        ],
+        'subdir2' => [],
+      ],
+      'test_3' => [],
+      'test_4' => [
+        'subdir1' => [
+          'item_4.test.yml' => "id: item4\nname: 'test4 item 4'",
+          'item_5.test.yml' => "id: item5\nname: 'test4 item 5'",
+          'item_6.test.yml' => "id: item6\nname: 'test4 item 6'",
+        ],
+      ],
+    ]);
+
+    // Set up the directories to search.
+    $directories = [
+      // Multiple directories both with valid items.
+      'test_1' => [
+        vfsStream::url('modules/test_1/subdir1'),
+        vfsStream::url('modules/test_1/subdir2'),
+      ],
+      // The subdir2 directory is empty.
+      'test_2' => [
+        vfsStream::url('modules/test_2/subdir1'),
+        vfsStream::url('modules/test_2/subdir2'),
+      ],
+      // Directories that do not exist.
+      'test_3' => [
+        vfsStream::url('modules/test_3/subdir1'),
+        vfsStream::url('modules/test_3/subdir2'),
+      ],
+      // A single directory.
+      'test_4' => vfsStream::url('modules/test_4/subdir1'),
+    ];
+
+    $discovery = new YamlDirectoryDiscovery($directories, 'test');
+    $data = $discovery->findAll();
+
+    $this->assertSame(['id' => 'item1', 'name' => 'test1 item 1', '_discovery_path' => 'vfs://modules/test_1/subdir1/item_1.test.yml'], $data['test_1']['item1']);
+    $this->assertSame(['id' => 'item2', 'name' => 'test1 item 2', '_discovery_path' => 'vfs://modules/test_1/subdir2/item_2.test.yml'], $data['test_1']['item2']);
+    $this->assertCount(2, $data['test_1']);
+
+    $this->assertSame(['id' => 'item3', 'name' => 'test2 item 3', '_discovery_path' => 'vfs://modules/test_2/subdir1/item_3.test.yml'], $data['test_2']['item3']);
+    $this->assertCount(1, $data['test_2']);
+
+    $this->assertTrue(empty($data['test_3']), 'test_3 provides 0 items');
+
+    $this->assertSame(['id' => 'item4', 'name' => 'test4 item 4', '_discovery_path' => 'vfs://modules/test_4/subdir1/item_4.test.yml'], $data['test_4']['item4']);
+    $this->assertSame(['id' => 'item5', 'name' => 'test4 item 5', '_discovery_path' => 'vfs://modules/test_4/subdir1/item_5.test.yml'], $data['test_4']['item5']);
+    $this->assertSame(['id' => 'item6', 'name' => 'test4 item 6', '_discovery_path' => 'vfs://modules/test_4/subdir1/item_6.test.yml'], $data['test_4']['item6']);
+    $this->assertCount(3, $data['test_4']);
+  }
+
+  /**
+   * Tests YAML directory discovery with an alternate ID key.
+   *
+   * @covers ::findAll
+   */
+  public function testDiscoveryAlternateId() {
+    vfsStream::setup('modules', NULL, [
+      'test_1' => [
+        'item_1.test.yml' => "alt_id: item1\nid: ignored",
+      ],
+    ]);
+
+    // Set up the directories to search.
+    $directories = ['test_1' => vfsStream::url('modules/test_1')];
+
+    $discovery = new YamlDirectoryDiscovery($directories, 'test', 'alt_id');
+    $data = $discovery->findAll();
+
+    $this->assertSame(['alt_id' => 'item1', 'id' => 'ignored', '_discovery_path' => 'vfs://modules/test_1/item_1.test.yml'], $data['test_1']['item1']);
+    $this->assertCount(1, $data['test_1']);
+  }
+
+  /**
+   * Tests YAML directory discovery with a missing ID key.
+   *
+   * @covers ::findAll
+   * @covers ::getIdentifier
+   */
+  public function testDiscoveryNoIdException() {
+    $this->setExpectedException(DiscoveryException::class, 'The vfs://modules/test_1/item_1.test.yml contains no data in the identifier key \'id\'');
+    vfsStream::setup('modules', NULL, [
+      'test_1' => [
+        'item_1.test.yml' => "",
+      ],
+    ]);
+
+    // Set up the directories to search.
+    $directories = ['test_1' => vfsStream::url('modules/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');
+    vfsStream::setup('modules', NULL, [
+      'test_1' => [
+        'item_1.test.yml' => "id: invalid\nfoo : [bar}",
+      ],
+    ]);
+
+    // Set up the directories to search.
+    $directories = ['test_1' => vfsStream::url('modules/test_1')];
+
+    $discovery = new YamlDirectoryDiscovery($directories, 'test');
+    $discovery->findAll();
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Plugin/Discovery/YamlDirectoryDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Plugin/Discovery/YamlDirectoryDiscoveryTest.php
new file mode 100644
index 0000000..f057f55
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Plugin/Discovery/YamlDirectoryDiscoveryTest.php
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Plugin\Discovery\YamlDiscoveryTest.
+ */
+
+namespace Drupal\Tests\Core\Plugin\Discovery;
+
+use Drupal\Component\Discovery\YamlDirectoryDiscovery as ComponentYamlDirectoryDiscovery;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Tests\UnitTestCase;
+use Drupal\Core\Plugin\Discovery\YamlDirectoryDiscovery;
+use org\bovigo\vfs\vfsStream;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Plugin\Discovery\YamlDirectoryDiscovery
+ *
+ * @group Plugin
+ */
+class YamlDirectoryDiscoveryTest extends UnitTestCase {
+
+  /**
+   * @covers ::getDefinitions
+   */
+  public function testGetDefinitions() {
+    vfsStream::setup('modules', NULL, [
+      'module_a' => [
+        'subdir1' => [
+          'plugin1.yml' => "id: plugin1\ntest_provider: module_a",
+          'plugin2.yml' => "id: plugin2\ntest_provider: module_a",
+        ],
+        'subdir2' => [
+          'plugin3.yml' => "id: plugin3\ntest_provider: module_a",
+        ],
+        'subdir3' => [
+        ]
+      ],
+      'module_b' => [
+        'subdir1' => [
+          'plugin4.yml' => "id: plugin4\ntest_provider: module_b",
+        ],
+      ],
+    ]);
+    $directories = [
+      'module_a' => [
+        vfsStream::url('modules/module_a/subdir1'),
+        vfsStream::url('modules/module_a/subdir2'),
+        // Empty directory.
+        vfsStream::url('modules/module_a/subdir3'),
+        // Directory does not exist.
+        vfsStream::url('modules/module_a/subdir4'),
+      ],
+      'module_b' => vfsStream::url('modules/module_b/subdir1'),
+    ];
+    $discovery = new YamlDirectoryDiscovery($directories, 'test');
+
+    $definitions = $discovery->getDefinitions();
+
+    $this->assertInternalType('array', $definitions);
+    $this->assertCount(4, $definitions);
+
+    foreach ($definitions as $id => $definition) {
+      foreach (array('id', 'provider', ComponentYamlDirectoryDiscovery::DISCOVERY_PATH) as $key) {
+        $this->assertArrayHasKey($key, $definition);
+      }
+      $this->assertEquals($id, $definition['id']);
+      $this->assertEquals($definition['test_provider'], $definition['provider']);
+    }
+  }
+
+  /**
+   * @covers ::getDefinitions
+   */
+  public function testGetDefinitionsWithTranslatableDefinitions() {
+    vfsStream::setup('modules', NULL, [
+      'module_a' => [
+        'subdir1' => [
+          'plugin1.yml' => "id: plugin1\ntest_provider: module_a\ntitle: 'test title'",
+          'plugin2.yml' => "id: plugin2\ntest_provider: module_a\ntitle: 'test title'\ntitle_context: test-context",
+        ],
+      ],
+    ]);
+    $directories = [
+      'module_a' => vfsStream::url('modules/module_a/subdir1'),
+    ];
+
+    $discovery = new YamlDirectoryDiscovery($directories, 'test');
+    $discovery->addTranslatableProperty('title', 'title_context');
+    $definitions = $discovery->getDefinitions();
+
+    $this->assertCount(2, $definitions);
+    $plugin_1 = $definitions['plugin1'];
+    $plugin_2 = $definitions['plugin2'];
+
+    $this->assertInstanceOf(TranslatableMarkup::class, $plugin_1['title']);
+    $this->assertEquals([], $plugin_1['title']->getOptions());
+    $this->assertInstanceOf(TranslatableMarkup::class, $plugin_2['title']);
+    $this->assertEquals(['context' => 'test-context'], $plugin_2['title']->getOptions());
+  }
+
+}
