diff --git a/config/schema/migrate_plus.schema.yml b/config/schema/migrate_plus.schema.yml
new file mode 100644
index 0000000..08b47b2
--- /dev/null
+++ b/config/schema/migrate_plus.schema.yml
@@ -0,0 +1,16 @@
+migrate_plus.migration_group.*:
+  type: config_entity
+  label: 'Migration Group'
+  mapping:
+    id:
+      type: string
+      label: 'ID'
+    label:
+      type: label
+      label: 'Label'
+    module:
+      type: string
+      label: 'Dependent module'
+    shared_configuration:
+      type: ignore
+      label: 'Shared migration configuration'
diff --git a/migrate_example/config/install/migrate.migration_group.beer.yml b/migrate_example/config/install/migrate.migration_group.beer.yml
deleted file mode 100644
index bcc5f2b..0000000
--- a/migrate_example/config/install/migrate.migration_group.beer.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-# The machine name of the group, by which it is referenced in individual
-# migrations.
-id: beer
-
-# A human-friendly label of the group, displayed in the UI.
-label: Beer Imports
-
-# Here we add any default configuration settings to be shared among all
-# migrations in the group.
-shared_configuration:
-  source:
-    key: default
diff --git a/migrate_example/config/install/migrate_plus.migration_group.beer.yml b/migrate_example/config/install/migrate_plus.migration_group.beer.yml
new file mode 100644
index 0000000..bcc5f2b
--- /dev/null
+++ b/migrate_example/config/install/migrate_plus.migration_group.beer.yml
@@ -0,0 +1,12 @@
+# The machine name of the group, by which it is referenced in individual
+# migrations.
+id: beer
+
+# A human-friendly label of the group, displayed in the UI.
+label: Beer Imports
+
+# Here we add any default configuration settings to be shared among all
+# migrations in the group.
+shared_configuration:
+  source:
+    key: default
diff --git a/migrate_example/migrate_example.info.yml b/migrate_example/migrate_example.info.yml
index b939410..8493aa6 100644
--- a/migrate_example/migrate_example.info.yml
+++ b/migrate_example/migrate_example.info.yml
@@ -6,3 +6,5 @@ core: 8.x
 dependencies:
   - migrate
   - migrate_example_setup
+  - migrate_plus
+
diff --git a/migrate_plus.drush.inc b/migrate_plus.drush.inc
index b9e5423..ec61df6 100644
--- a/migrate_plus.drush.inc
+++ b/migrate_plus.drush.inc
@@ -87,10 +87,8 @@ function drush_migrate_plus_migrate_status($migration_names = '') {
       catch (Exception $e) {
         continue;
       }
-      // We can't get counts from an EmptySource plugin, or one without a query.
-      // @todo: Is there a more general approach? Are there other edge cases?
       try {
-        if (is_a($source_plugin, 'Drupal\migrate\Plugin\migrate\source\EmptySource')) {
+        if (!method_exists($source_plugin, 'count')) {
           $source_rows = dt('N/A');
           $unprocessed = dt('N/A');
         }
@@ -156,7 +154,7 @@ function drush_migrate_plus_migrate_import($migration_names = '') {
 function drush_migrate_plus_migration_list($group_id = '', $migration_ids = '') {
   $query = \Drupal::entityQuery('migration');
   if ($group_id) {
-    $query->condition('migration_groups.*', $group_id);
+    $query->condition('migration_group', $group_id);
   }
   $names = $query->execute();
 
@@ -176,17 +174,10 @@ function drush_migrate_plus_migration_list($group_id = '', $migration_ids = '')
   $return = array();
   foreach ($migrations as $migration_id => $migration) {
     if (empty($migration_ids) || in_array(Unicode::strtolower($migration_id), $migration_ids)) {
-      if (is_array($migration->migration_groups)) {
-        $group_id = reset($migration->migration_groups);
-        // @todo: Temporary - ignore incomplete migrate_drupal migrations.
-        if (substr($group_id, 0, 7) == 'Drupal ' && !$migration->get('source.key')) {
-          continue;
-        }
-      }
-      else {
-        $group_id = 'default';
+      $group_id = $migration->get('migration_group');
+      if (!empty($group_id)) {
+        $return[$group_id][$migration_id] = $migration;
       }
-      $return[$group_id][$migration_id] = $migration;
     }
   }
   return $return;
diff --git a/migrate_plus.module b/migrate_plus.module
new file mode 100644
index 0000000..66a30b0
--- /dev/null
+++ b/migrate_plus.module
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Provides tools and enhancements for implementing and managing migrations.
+ */
+
+/**
+ * Implements hook_entity_type_alter().
+ */
+function migrate_plus_entity_type_alter(array &$entity_types) {
+  // Substitute our own classes to provide enhanced functionality to the
+  // migration entity type.
+  /** @var \Drupal\Core\Config\Entity\ConfigEntityType[] $entity_types */
+  $entity_types['migration']
+    ->setHandlerClass('storage', 'Drupal\migrate_plus\MigrationStorage');
+}
+
+/**
+ * Implements hook_config_schema_info_alter().
+ */
+function migrate_plus_config_schema_info_alter(&$definitions) {
+  // Add a migration_group key to the Migration configuration schema, so
+  // migrations can reference their containing group.
+  $definitions['migrate.migration.*']['mapping']['migration_group'] =
+    array(
+      'type' => 'string',
+      'label' => 'Group',
+    );
+}
diff --git a/src/Entity/MigrationGroup.php b/src/Entity/MigrationGroup.php
new file mode 100644
index 0000000..628a2b1
--- /dev/null
+++ b/src/Entity/MigrationGroup.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate_plus\Entity\MigrationGroup.
+ */
+
+namespace Drupal\migrate_plus\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\migrate\Entity\MigrationInterface;
+
+/**
+ * Defines the Migration Group entity.
+ *
+ * The migration group entity is used to group active migrations, as well as to
+ * store shared migration configuration.
+ *
+ * @ConfigEntityType(
+ *   id = "migration_group",
+ *   label = @Translation("Migration Group"),
+ *   module = "migrate_plus",
+ *   handlers = {
+ *   },
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label"
+ *   }
+ * )
+ */
+class MigrationGroup extends ConfigEntityBase implements MigrationGroupInterface {
+
+  /**
+   * The migration group ID (machine name).
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The human-readable label for the migration group.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete() {
+    // Delete all migrations contained in this group.
+    $query = \Drupal::entityQuery('migration')
+      ->condition('migration_group', $this->id());
+    $names = $query->execute();
+
+    // Order the migrations according to their dependencies.
+    /** @var MigrationInterface[] $migrations */
+    $migrations = \Drupal::entityManager()->getStorage('migration')->loadMultiple($names);
+
+    // Delete in reverse order, so dependencies are never violated.
+    $migrations = array_reverse($migrations);
+
+    foreach ($migrations as $migration) {
+      $migration->delete();
+    }
+
+    // Finally, delete the group itself.
+    parent::delete();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    parent::calculateDependencies();
+    // Make sure we save any explicit module dependencies.
+    if ($provider = $this->get('module')) {
+      $this->addDependency('module', $provider);
+    }
+    return $this->dependencies;
+  }
+
+}
diff --git a/src/Entity/MigrationGroupInterface.php b/src/Entity/MigrationGroupInterface.php
new file mode 100644
index 0000000..ff2c855
--- /dev/null
+++ b/src/Entity/MigrationGroupInterface.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Entity\MigrationGroupInterface.
+ */
+
+namespace Drupal\migrate_plus\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+
+/**
+ * Interface for migration groups.
+ */
+interface MigrationGroupInterface extends ConfigEntityInterface {
+
+}
diff --git a/src/MigrationStorage.php b/src/MigrationStorage.php
new file mode 100644
index 0000000..8a23be5
--- /dev/null
+++ b/src/MigrationStorage.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate_plus\MigrateStorage.
+ */
+
+namespace Drupal\migrate_plus;
+
+use Drupal\Component\Uuid\UuidInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\migrate\Entity\MigrationInterface;
+use Drupal\migrate_plus\Entity\MigrationGroupInterface;
+use Drupal\migrate\MigrationStorage as BaseMigrationStorage;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Storage for migration entities.
+ */
+class MigrationStorage extends BaseMigrationStorage {
+
+  /**
+   * The migration group storage.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface
+   */
+  protected $migrationGroupStorage;
+
+  /**
+   * Constructs a MigrationStorage object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory service.
+   * @param \Drupal\Component\Uuid\UuidInterface $uuid_service
+   *   The UUID service.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $migration_group_storage
+   *   The migration group storage.
+   */
+  public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, EntityStorageInterface $migration_group_storage) {
+    parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
+    $this->migrationGroupStorage = $migration_group_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('config.factory'),
+      $container->get('uuid'),
+      $container->get('language_manager'),
+      $container->get('entity.manager')->getStorage('migration_group')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doLoadMultiple(array $ids = NULL) {
+    $migrations = parent::doLoadMultiple($ids);
+
+    /** @var MigrationInterface $migration */
+    foreach ($migrations as $migration) {
+      // If we are pointing to a valid group, merge its properties into ours.
+      if (isset($migration->migration_group)) {
+        /** @var MigrationGroupInterface $group */
+        $group = $this->migrationGroupStorage->load($migration->migration_group);
+        if ($group) {
+          $shared_configuration = $group->get('shared_configuration');
+          if ($shared_configuration) {
+            foreach ($shared_configuration as $key => $group_value) {
+              $migration_value = $migration->get($key);
+              // Where both the migration and the group provide arrays,
+              // replace recursively (so each key collision is resolved in favor
+              // of the migration).
+              if (is_array($migration_value) && is_array($group_value)) {
+                $merged_values = array_replace_recursive($group_value, $migration_value);
+                $migration->set($key, $merged_values);
+              }
+              // Where the group provides a value the migration doesn't, use
+              // the group value.
+              elseif (is_null($migration_value)) {
+                $migration->set($key, $group_value);
+              }
+              // Otherwise, the existing migration value overrides the group
+              // value.
+            }
+          }
+        }
+      }
+    }
+    return $migrations;
+  }
+
+}
diff --git a/src/Tests/MigrationGroupTest.php b/src/Tests/MigrationGroupTest.php
new file mode 100644
index 0000000..316cf74
--- /dev/null
+++ b/src/Tests/MigrationGroupTest.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate_plus\Tests\MigrationGroupTest.
+ */
+
+namespace Drupal\migrate_plus\Tests;
+
+use Drupal\migrate\Entity\MigrationInterface;
+use Drupal\migrate_plus\Entity\MigrationGroupInterface;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Test migration groups.
+ *
+ * @group migrate_plus
+ */
+class MigrationGroupTest extends WebTestBase {
+
+  public static $modules = array('migrate_plus');
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+  }
+
+  /**
+   * Test that group configuration is properly merged into specific migrations.
+   */
+  public function testConfigurationMerge() {
+    $group_id = 'test_group';
+
+    /** @var MigrationGroupInterface $migration_group */
+    $migration_group = entity_create('migration_group', array());
+    $migration_group->set('id', $group_id);
+    $migration_group->set('shared_configuration', array(
+      'migration_tags' => array('Drupal 6'), // In migration, so will be overridden.
+      'source' => array(
+        'constants' => array(
+          'type' => 'image',    // Not in migration, so will be added.
+          'cardinality' => '1', // In migration, so will be overridden.
+        ),
+      ),
+      'destination' => array('plugin' => 'field_storage_config'), // Not in migration, so will be added.
+    ));
+    $migration_group->save();
+
+    /** @var MigrationInterface $migration */
+    $migration = entity_create('migration', array(
+      'id' => 'specific_migration',
+      'load' => [],
+      'migration_group' => $group_id,
+      'label' => 'Unaffected by the group',
+      'migration_tags' => array('Drupal 7'), // Overrides group.
+      'destination' => array(),
+      'source' => array(),
+    ));
+    $migration->set('source', array(
+      'plugin' => 'empty',        // Not in group, persists.
+      'constants' => array(
+        'entity_type' => 'user',  // Not in group, persists.
+        'cardinality' => '3',     // Overrides group.
+      ),
+    ));
+    $migration->save();
+
+    $expected_config = array(
+      'migration_group' => $group_id,
+      'label' => 'Unaffected by the group',
+      'migration_tags' => array('Drupal 7'),
+      'source' => array(
+        'plugin' => 'empty',
+        'constants' => array(
+          'entity_type' => 'user',
+          'type' => 'image',
+          'cardinality' => '3',
+        ),
+      ),
+      'destination' => array('plugin' => 'field_storage_config'),
+    );
+    /** @var MigrationInterface $loaded_migration */
+    $loaded_migration = entity_load('migration', 'specific_migration', TRUE);
+    foreach ($expected_config as $key => $expected_value) {
+      $actual_value = $loaded_migration->get($key);
+      $this->assertEqual($expected_value, $actual_value);
+    }
+  }
+
+  /**
+   * Test that deleting a group deletes its migrations.
+   */
+  public function testDelete() {
+    /** @var MigrationGroupInterface $migration_group */
+    $migration_group = entity_create('migration_group', array());
+    $migration_group->set('id', 'test_group');
+    $migration_group->save();
+
+    /** @var MigrationInterface $migration */
+    $migration = entity_create('migration', [
+      'id' => 'specific_migration',
+      'migration_group' => 'test_group',
+      'migration_tags' => array(),
+      'load' => [],
+      'destination' => array(),
+      'source' => array(),
+    ]);
+    $migration->save();
+
+    /** @var MigrationGroupInterface $loaded_migration_group */
+    $loaded_migration_group = entity_load('migration_group', 'test_group', TRUE);
+    $loaded_migration_group->delete();
+
+    /** @var MigrationGroupInterface $loaded_migration */
+    $loaded_migration = entity_load('migration', 'specific_migration', TRUE);
+    $this->assertNull($loaded_migration);
+  }
+
+}
