diff --git a/src/Datasource/DatasourcePluginBase.php b/src/Datasource/DatasourcePluginBase.php
index 2f1fa8b..3a91803 100644
--- a/src/Datasource/DatasourcePluginBase.php
+++ b/src/Datasource/DatasourcePluginBase.php
@@ -41,13 +41,49 @@ abstract class DatasourcePluginBase extends IndexPluginBase implements Datasourc
   /**
    * {@inheritdoc}
    */
-  public function getBundles() {
+  public function getPropertyDefinitions() {
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function load($id) {
+    $items = $this->loadMultiple(array($id));
+    return $items ? reset($items) : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadMultiple(array $ids) {
     return array();
   }
 
   /**
    * {@inheritdoc}
    */
+  public function getItemLabel(ComplexDataInterface $item) {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemBundle(ComplexDataInterface $item) {
+    return $this->getPluginId();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemUrl(ComplexDataInterface $item) {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getViewModes($bundle = NULL) {
     return array();
   }
@@ -55,6 +91,13 @@ abstract class DatasourcePluginBase extends IndexPluginBase implements Datasourc
   /**
    * {@inheritdoc}
    */
+  public function getBundles() {
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function viewItem(ComplexDataInterface $item, $view_mode, $langcode = NULL) {
     return array();
   }
@@ -70,4 +113,18 @@ abstract class DatasourcePluginBase extends IndexPluginBase implements Datasourc
     return $build;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityTypeId() {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemIds($page = NULL) {
+    return array();
+  }
+
 }
diff --git a/src/Entity/Index.php b/src/Entity/Index.php
index e9a5057..14191af 100644
--- a/src/Entity/Index.php
+++ b/src/Entity/Index.php
@@ -10,7 +10,6 @@ namespace Drupal\search_api\Entity;
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
-use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\search_api\IndexInterface;
 use Drupal\search_api\Item\FieldInterface;
@@ -1342,24 +1341,111 @@ class Index extends ConfigEntityBase implements IndexInterface {
    * {@inheritdoc}
    */
   public function calculateDependencies() {
-    parent::calculateDependencies();
+    $dependencies = $this->getDependencyData();
+    $this->dependencies = array();
 
-    // Add a dependency on the server, if there is one set.
-    if ($this->hasValidServer()) {
-      $this->addDependency('config', $this->getServer()->getConfigDependencyName());
+    foreach ($dependencies as $type => $list) {
+      $this->dependencies[$type] = array_keys($list);
     }
-    // Add dependencies for all of the index's plugins.
-    if ($this->hasValidTracker()) {
-      $this->calculatePluginDependencies($this->getTracker());
+
+    return $this;
+  }
+
+  /**
+   * Retrieves data about this index's dependencies.
+   *
+   * The return value is structured as follows:
+   *
+   * @code
+   * array(
+   *   'config' => array(
+   *     'CONFIG_DEPENDENCY_KEY' => array(
+   *       'always' => array(
+   *         'processors' => array(
+   *           'PROCESSOR_ID' => $processor,
+   *         ),
+   *         'datasources' => array(
+   *           'DATASOURCE_ID_1' => $datasource_1,
+   *           'DATASOURCE_ID_2' => $datasource_2,
+   *         ),
+   *       ),
+   *       'optional' => array(
+   *         'index' => array(
+   *           'INDEX_ID' => $index,
+   *         ),
+   *         'tracker' => array(
+   *           'TRACKER_ID' => $tracker,
+   *         ),
+   *       ),
+   *     ),
+   *   )
+   * )
+   * @endcode
+   *
+   * @return object[][][][][]
+   *   An associative array containing the index's dependencies. The array is
+   *   first keyed by the config dependency type ("module", "config", etc.) and
+   *   then by the names of the config dependencies of that type which the index
+   *   has. The values are associative arrays with up to two keys, "always" and
+   *   "optional", specifying whether the dependency is a hard one by the plugin
+   *   (or index) in question or potentially depending on the configuration. The
+   *   values on this level are arrays with keys "index", "tracker",
+   *   "datasources" and/or "processors" and values arrays of IDs mapped to
+   *   their entities/plugins.
+   */
+  protected function getDependencyData() {
+    $dependency_data = array();
+
+    // Since calculateDependencies() will work directly on the $dependencies
+    // property, we first save its original state and then restore it
+    // afterwards.
+    $original_dependencies = $this->dependencies;
+    parent::calculateDependencies();
+    foreach ($this->dependencies as $dependency_type => $list) {
+      foreach ($list as $name) {
+        $dependency_data[$dependency_type][$name]['always']['index'][$this->id] = $this;
+      }
     }
-    foreach ($this->getProcessors() as $processor) {
-      $this->calculatePluginDependencies($processor);
+    $this->dependencies = $original_dependencies;
+
+    // The server needs special treatment, since it is a dependency of the index
+    // itself, and not one of its plugins.
+    if ($this->hasValidServer()) {
+      $name = $this->getServer()->getConfigDependencyName();
+      $dependency_data['config'][$name]['optional']['index'][$this->id] = $this;
     }
-    foreach ($this->getDatasources() as $datasource) {
-      $this->calculatePluginDependencies($datasource);
+
+    // All other plugins can be treated uniformly.
+    $plugins = $this->getAllPlugins();
+
+    foreach ($plugins as $plugin_type => $type_plugins) {
+      foreach ($type_plugins as $plugin_id => $plugin) {
+        // Largely copied from
+        // \Drupal\Core\Plugin\PluginDependencyTrait::calculatePluginDependencies().
+        $definition = $plugin->getPluginDefinition();
+
+        // First, always depend on the module providing the plugin.
+        $dependency_data['module'][$definition['provider']]['always'][$plugin_type][$plugin_id] = $plugin;
+
+        // Plugins can declare additional dependencies in their definition.
+        if (isset($definition['config_dependencies'])) {
+          foreach ($definition['config_dependencies'] as $dependency_type => $list) {
+            foreach ($list as $name) {
+              $dependency_data[$dependency_type][$name]['always'][$plugin_type][$plugin_id] = $plugin;
+            }
+          }
+        }
+
+        // Finally, add the dynamically-calculated dependencies of the plugin.
+        foreach ($plugin->calculateDependencies() as $dependency_type => $list) {
+          foreach ($list as $name) {
+            $dependency_data[$dependency_type][$name]['optional'][$plugin_type][$plugin_id] = $plugin;
+          }
+        }
+      }
     }
 
-    return $this->dependencies;
+    return $dependency_data;
   }
 
   /**
@@ -1368,21 +1454,130 @@ class Index extends ConfigEntityBase implements IndexInterface {
   public function onDependencyRemoval(array $dependencies) {
     $changed = parent::onDependencyRemoval($dependencies);
 
-    // @todo Also react sensibly when removing the dependency of a plugin or an
-    //   indexed field. See #2574633 and #2541206.
-    foreach ($dependencies['config'] as $entity) {
-      if ($entity instanceof EntityInterface && $entity->getEntityTypeId() == 'search_api_server') {
-        // Remove this index from the deleted server (thus disabling it).
-        $this->setServer(NULL);
-        $this->setStatus(FALSE);
-        $changed = TRUE;
+    $all_plugins = $this->getAllPlugins();
+    $dependency_data = $this->getDependencyData();
+    // Make sure our dependency data has the exact same keys as $dependencies,
+    // to simplify the subsequent code.
+    $dependencies = array_filter($dependencies);
+    $dependency_data = array_intersect_key($dependency_data, $dependencies);
+    $dependency_data += array_fill_keys(array_keys($dependencies), array());
+    $call_on_removal = array();
+
+    foreach ($dependencies as $dependency_type => $dependency_objects) {
+      $dependency_data[$dependency_type] = array_intersect_key($dependency_data[$dependency_type], $dependency_objects);
+      foreach ($dependency_data[$dependency_type] as $name => $dependency_sources) {
+        // We first remove all the "hard" dependencies.
+        if (!empty($dependency_sources['always'])) {
+          foreach ($dependency_sources['always'] as $plugin_type => $plugins) {
+            // We can hardly remove the index itself.
+            if ($plugin_type == 'index') {
+              continue;
+            }
+
+            $all_plugins[$plugin_type] = array_diff_key($all_plugins[$plugin_type], $plugins);
+            $changed = TRUE;
+          }
+        }
+
+        // Then, collect all the optional ones.
+        if (!empty($dependency_sources['optional'])) {
+          // However this plays out, it will lead to a change.
+          $changed = TRUE;
+
+          foreach ($dependency_sources['optional'] as $plugin_type => $plugins) {
+            // Deal with the index right away, since that dependency can only be
+            // the server.
+            if ($plugin_type == 'index') {
+              $this->setServer(NULL);
+              continue;
+            }
+
+            // Only include those plugins that have not already been removed.
+            $plugins = array_intersect_key($plugins, $all_plugins[$plugin_type]);
+
+            foreach ($plugins as $plugin_id => $plugin) {
+              $call_on_removal[$plugin_type][$plugin_id][$dependency_type][$name] = $dependency_objects[$name];
+            }
+          }
+        }
+      }
+    }
+
+    $updated_config = array();
+    foreach ($call_on_removal as $plugin_type => $plugins) {
+      foreach ($plugins as $plugin_id => $plugin_dependencies) {
+        $removal_successful = $all_plugins[$plugin_type][$plugin_id]->onDependencyRemoval($plugin_dependencies);
+        if ($removal_successful) {
+          $updated_config[$plugin_type][$plugin_id] = $all_plugins[$plugin_type][$plugin_id]->getConfiguration();
+        }
+        else {
+          unset($all_plugins[$plugin_type][$plugin_id]);
+        }
       }
     }
 
+    // The handling of how we translate plugin changes back to the index varies
+    // according to plugin type, unfortunately.
+    // First, remove plugins that need to be removed.
+    $this->processors = array_intersect_key($this->processors, $all_plugins['processors']);
+    $this->datasources = array_keys($all_plugins['datasources']);
+    $this->datasource_configs = array_intersect_key($this->datasource_configs, $all_plugins['datasources']);
+    // There always needs to be a tracker.
+    if (empty($all_plugins['tracker'])) {
+      $this->tracker = \Drupal::config('search_api.settings')->get('default_tracker');
+      $this->tracker_config = array();
+    }
+    // There also always needs to be a datasource, but here we have no easy way
+    // out – if we had to remove all datasources, the operation fails. Return
+    // FALSE to indicate this (and hope this means the right thing).
+    if (!$this->datasources) {
+      return FALSE;
+    }
+
+    // Then, update configuration as necessary.
+    foreach ($updated_config as $plugin_type => $plugin_configs) {
+      foreach ($plugin_configs as $plugin_id => $plugin_config) {
+        switch ($plugin_type) {
+          case 'processors':
+            $this->processors[$plugin_id]['settings'] = $plugin_config;
+            break;
+          case 'datasources':
+            $this->datasource_configs[$plugin_id] = $plugin_config;
+            break;
+          case 'tracker':
+            $this->tracker_config = $plugin_config;
+            break;
+        }
+      }
+    }
+
+    if ($changed) {
+      $this->resetCaches();
+    }
+
     return $changed;
   }
 
   /**
+   * Retrieves all the plugins contained in this index.
+   *
+   * @return \Drupal\search_api\Plugin\IndexPluginInterface[][]
+   *   All plugins contained in this index, keyed by their property on the index
+   *   and their plugin ID.
+   */
+  protected function getAllPlugins() {
+    $plugins = array();
+
+    if ($this->hasValidTracker()) {
+      $plugins['tracker'][$this->getTrackerId()] = $this->getTracker();
+    }
+    $plugins['processors'] = $this->getProcessors();
+    $plugins['datasources'] = $this->getDatasources();
+
+    return $plugins;
+  }
+
+  /**
    * Implements the magic __clone() method.
    *
    * Prevents the cached plugins and fields from being cloned, too (since they
diff --git a/src/Entity/Server.php b/src/Entity/Server.php
index f3e083a..1f7c0a0 100644
--- a/src/Entity/Server.php
+++ b/src/Entity/Server.php
@@ -441,11 +441,37 @@ class Server extends ConfigEntityBase implements ServerInterface {
     parent::calculateDependencies();
 
     // Add the backend's dependencies.
-    if ($this->hasValidBackend() && ($backend = $this->getBackend())) {
-      $this->addDependencies($backend->calculateDependencies());
+    if ($this->hasValidBackend()) {
+      $this->calculatePluginDependencies($this->getBackend());
+    }
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onDependencyRemoval(array $dependencies) {
+    $changed = parent::onDependencyRemoval($dependencies);
+
+    if ($this->hasValidBackend()) {
+      $removed_backend_dependencies = array();
+      $backend = $this->getBackend();
+      foreach ($backend->calculateDependencies() as $dependency_type => $list) {
+        if (isset($dependencies[$dependency_type])) {
+          $removed_backend_dependencies[$dependency_type] = array_intersect_key($dependencies[$dependency_type], array_flip($list));
+        }
+      }
+      $removed_backend_dependencies = array_filter($removed_backend_dependencies);
+      if ($removed_backend_dependencies) {
+        if ($backend->onDependencyRemoval($removed_backend_dependencies)) {
+          $this->backend_config = $backend->getConfiguration();
+          $changed = TRUE;
+        }
+      }
     }
 
-    return $this->dependencies;
+    return $changed;
   }
 
   /**
diff --git a/src/Plugin/ConfigurablePluginBase.php b/src/Plugin/ConfigurablePluginBase.php
index d3edd25..9c8f78d 100644
--- a/src/Plugin/ConfigurablePluginBase.php
+++ b/src/Plugin/ConfigurablePluginBase.php
@@ -100,8 +100,16 @@ abstract class ConfigurablePluginBase extends PluginBase implements Configurable
    * {@inheritdoc}
    */
   public function calculateDependencies() {
-    $this->addDependency('module', $this->getPluginDefinition()['provider']);
-    return $this->dependencies;
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onDependencyRemoval(array $dependencies) {
+    // By default, we're not reacting to anything and so we should leave
+    // everything as it was.
+    return FALSE;
   }
 
 }
diff --git a/src/Plugin/ConfigurablePluginInterface.php b/src/Plugin/ConfigurablePluginInterface.php
index e937f42..67227bd 100644
--- a/src/Plugin/ConfigurablePluginInterface.php
+++ b/src/Plugin/ConfigurablePluginInterface.php
@@ -35,4 +35,23 @@ interface ConfigurablePluginInterface extends PluginInspectionInterface, Derivat
    */
   public function getDescription();
 
+  /**
+   * Informs the plugin that some of its dependencies are being removed.
+   *
+   * The plugin should attempt to change its configuration in a way to remove
+   * its dependency on those items. However, to avoid problems, it should (as
+   * far as possible) not add any new dependencies in the process, since there
+   * is no guarantee that those are not currently being removed, too.
+   *
+   * @param object[][] $dependencies
+   *   An array of dependencies, keyed by dependency type ("module", "config",
+   *   etc.) and dependency name.
+   *
+   * @return bool
+   *   Whether the dependency was successfully removed from the plugin – i.e.,
+   *   after the configuration changes that were made, none of the removed
+   *   items are dependencies of this plugin anymore.
+   */
+  public function onDependencyRemoval(array $dependencies);
+
 }
diff --git a/src/Plugin/search_api/datasource/ContentEntity.php b/src/Plugin/search_api/datasource/ContentEntity.php
index 3c53b91..8bb2378 100644
--- a/src/Plugin/search_api/datasource/ContentEntity.php
+++ b/src/Plugin/search_api/datasource/ContentEntity.php
@@ -350,14 +350,6 @@ class ContentEntity extends DatasourcePluginBase {
   /**
    * {@inheritdoc}
    */
-  public function load($id) {
-    $items = $this->loadMultiple(array($id));
-    return $items ? reset($items) : NULL;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   public function loadMultiple(array $ids) {
     $allowed_languages = $all_languages = $this->getLanguageManager()->getLanguages();
 
diff --git a/src/Plugin/search_api/processor/RenderedItem.php b/src/Plugin/search_api/processor/RenderedItem.php
index 68069d4..b817e34 100644
--- a/src/Plugin/search_api/processor/RenderedItem.php
+++ b/src/Plugin/search_api/processor/RenderedItem.php
@@ -315,4 +315,36 @@ class RenderedItem extends ProcessorPluginBase {
     return $this->dependencies;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function onDependencyRemoval(array $dependencies) {
+    // Make arrays of dependencies associative to simplify the code.
+    foreach (array_keys($dependencies) as $dependency_type) {
+      $dependencies[$dependency_type] = array_combine($dependencies[$dependency_type], $dependencies[$dependency_type]);
+    }
+
+    $view_modes = $this->configuration['view_mode'];
+    foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
+      if ($entity_type_id = $datasource->getEntityTypeId() && !empty($view_modes[$datasource_id])) {
+        foreach ($view_modes[$datasource_id] as $bundle => $view_mode_id) {
+          if ($view_mode_id) {
+            /** @var \Drupal\Core\Entity\EntityViewModeInterface $view_mode */
+            $view_mode = EntityViewMode::load($entity_type_id . '.' . $view_mode_id);
+            if ($view_mode) {
+              $dependency_key = $view_mode->getConfigDependencyKey();
+              $dependency_name = $view_mode->getConfigDependencyName();
+              if (!empty($dependencies[$dependency_key][$dependency_name])) {
+                unset($this->configuration['view_mode'][$datasource_id][$bundle]);
+                unset($dependencies[$dependency_key][$dependency_name]);
+              }
+            }
+          }
+        }
+      }
+    }
+
+    return empty($dependencies);
+  }
+
 }
diff --git a/src/Tests/HooksTest.php b/src/Tests/HooksTest.php
index b9aa86c..348549c 100644
--- a/src/Tests/HooksTest.php
+++ b/src/Tests/HooksTest.php
@@ -20,13 +20,6 @@ class HooksTest extends WebTestBase {
   public static $modules = array('node', 'search_api', 'search_api_test_backend', 'search_api_test_views', 'search_api_test_hooks');
 
   /**
-   * The id of the index.
-   *
-   * @var string
-   */
-  protected $indexId;
-
-  /**
    * {@inheritdoc}
    */
   public function setUp() {
@@ -45,8 +38,7 @@ class HooksTest extends WebTestBase {
 
     // Create an index and server to work with.
     $this->getTestServer();
-    $index = $this->getTestIndex();
-    $this->indexId = $index->id();
+    $this->getTestIndex();
 
     // Log in, so we can test all the things.
     $this->drupalLogin($this->adminUser);
@@ -103,21 +95,4 @@ class HooksTest extends WebTestBase {
     $this->assertText('Llama');
   }
 
-  /**
-   * Returns the system path for the test index.
-   *
-   * @param string|null $tab
-   *   (optional) If set, the path suffix for a specific index tab.
-   *
-   * @return string
-   *   A system path.
-   */
-  protected function getIndexPath($tab = NULL) {
-    $path = 'admin/config/search/search-api/index/' . $this->indexId;
-    if ($tab) {
-      $path .= "/$tab";
-    }
-    return $path;
-  }
-
 }
diff --git a/src/Tests/IntegrationTest.php b/src/Tests/IntegrationTest.php
index 570f5e6..cb3dd22 100644
--- a/src/Tests/IntegrationTest.php
+++ b/src/Tests/IntegrationTest.php
@@ -28,13 +28,6 @@ class IntegrationTest extends WebTestBase {
   protected $serverId;
 
   /**
-   * The ID of the search index used for this test.
-   *
-   * @var string
-   */
-  protected $indexId;
-
-  /**
    * A storage instance for indexes.
    *
    * @var \Drupal\Core\Entity\EntityStorageInterface
@@ -852,23 +845,6 @@ class IntegrationTest extends WebTestBase {
   }
 
   /**
-   * Returns the system path for the test index.
-   *
-   * @param string|null $tab
-   *   (optional) If set, the path suffix for a specific index tab.
-   *
-   * @return string
-   *   A system path.
-   */
-  protected function getIndexPath($tab = NULL) {
-    $path = 'admin/config/search/search-api/index/' . $this->indexId;
-    if ($tab) {
-      $path .= "/$tab";
-    }
-    return $path;
-  }
-
-  /**
    * Retrieves test index.
    *
    * @return \Drupal\search_api\IndexInterface
diff --git a/src/Tests/Processor/ProcessorIntegrationTest.php b/src/Tests/Processor/ProcessorIntegrationTest.php
index 912b715..ad617c8 100644
--- a/src/Tests/Processor/ProcessorIntegrationTest.php
+++ b/src/Tests/Processor/ProcessorIntegrationTest.php
@@ -21,13 +21,6 @@ use Drupal\search_api\Tests\WebTestBase;
 class ProcessorIntegrationTest extends WebTestBase {
 
   /**
-   * The ID of the search index used by this test.
-   *
-   * @var string
-   */
-  protected $indexId;
-
-  /**
    * {@inheritdoc}
    */
   public function setUp() {
diff --git a/src/Tests/WebTestBase.php b/src/Tests/WebTestBase.php
index f19efd0..a2d7734 100644
--- a/src/Tests/WebTestBase.php
+++ b/src/Tests/WebTestBase.php
@@ -32,6 +32,15 @@ abstract class WebTestBase extends SimpletestWebTestBase {
    * @var \Drupal\Core\Session\AccountInterface
    */
   protected $adminUser;
+  /**
+   * The permissions of the admin user.
+   *
+   * @var string[]
+  */
+  protected $adminUserPermissions = array(
+    'administer search_api',
+    'access administration pages'
+  );
 
   /**
    * A user without Search API admin permission.
@@ -55,13 +64,20 @@ abstract class WebTestBase extends SimpletestWebTestBase {
   protected $urlGenerator;
 
   /**
+   * The ID of the search index used for this test.
+   *
+   * @var string
+   */
+  protected $indexId;
+
+  /**
    * {@inheritdoc}
    */
   public function setUp() {
     parent::setUp();
 
     // Create the users used for the tests.
-    $this->adminUser = $this->drupalCreateUser(array('administer search_api', 'access administration pages'));
+    $this->adminUser = $this->drupalCreateUser($this->adminUserPermissions);
     $this->unauthorizedUser = $this->drupalCreateUser(array('access administration pages'));
     $this->anonymousUser = $this->drupalCreateUser();
 
@@ -101,6 +117,7 @@ abstract class WebTestBase extends SimpletestWebTestBase {
    */
   public function getTestServer($name = 'WebTest server', $id = 'webtest_server', $backend_id = 'search_api_test_backend', $backend_config = array(), $reset = FALSE) {
     if ($reset) {
+      /** @var \Drupal\search_api\ServerInterface $server */
       $server = Server::load($id);
       if ($server) {
         $server->delete();
@@ -140,6 +157,7 @@ abstract class WebTestBase extends SimpletestWebTestBase {
    */
   public function getTestIndex($name = 'WebTest Index', $id = 'webtest_index', $server_id = 'webtest_server', $datasource_id = 'entity:node', $reset = FALSE) {
     if ($reset) {
+      /** @var \Drupal\search_api\IndexInterface $index */
       $index = Index::load($id);
       if ($index) {
         $index->delete();
@@ -154,9 +172,27 @@ abstract class WebTestBase extends SimpletestWebTestBase {
         'datasources' => array($datasource_id),
       ));
       $index->save();
+      $this->indexId = $index->id();
     }
 
     return $index;
   }
 
+  /**
+   * Returns the system path for the test index.
+   *
+   * @param string|null $tab
+   *   (optional) If set, the path suffix for a specific index tab.
+   *
+   * @return string
+   *   A system path.
+   */
+  protected function getIndexPath($tab = NULL) {
+    $path = 'admin/config/search/search-api/index/' . $this->indexId;
+    if ($tab) {
+      $path .= "/$tab";
+    }
+    return $path;
+  }
+
 }
diff --git a/tests/search_api_test_backend/config/schema/search_api_test_backend.schema.yml b/tests/search_api_test_backend/config/schema/search_api_test_backend.schema.yml
index 12c982a..561b9fe 100644
--- a/tests/search_api_test_backend/config/schema/search_api_test_backend.schema.yml
+++ b/tests/search_api_test_backend/config/schema/search_api_test_backend.schema.yml
@@ -5,3 +5,6 @@ search_api.backend.plugin.search_api_test_backend:
     test:
       type: string
       label: Test configuration
+    dependencies:
+      type: config_dependencies
+      label: Dependencies
diff --git a/tests/search_api_test_backend/src/Plugin/search_api/backend/TestBackend.php b/tests/search_api_test_backend/src/Plugin/search_api/backend/TestBackend.php
index 93d60c1..5032989 100644
--- a/tests/search_api_test_backend/src/Plugin/search_api/backend/TestBackend.php
+++ b/tests/search_api_test_backend/src/Plugin/search_api/backend/TestBackend.php
@@ -174,6 +174,25 @@ class TestBackend extends BackendPluginBase {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return !empty($this->configuration['dependencies']) ? $this->configuration['dependencies'] : array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onDependencyRemoval(array $dependencies) {
+    $key = 'search_api_test_backend.dependencies.remove';
+    $remove = \Drupal::state()->get($key, FALSE);
+    if ($remove) {
+      unset($this->configuration['dependencies']);
+    }
+    return $remove;
+  }
+
+  /**
    * Throws an exception if set in the Drupal state for the given method.
    *
    * Also records (successful) calls to these methods.
@@ -194,4 +213,5 @@ class TestBackend extends BackendPluginBase {
     $methods_called[] = $method;
     $state->set($key, $methods_called);
   }
+
 }
diff --git a/tests/search_api_test_dependencies/config/schema/search_api_test_dependencies.schema.yml b/tests/search_api_test_dependencies/config/schema/search_api_test_dependencies.schema.yml
new file mode 100644
index 0000000..a96bca8
--- /dev/null
+++ b/tests/search_api_test_dependencies/config/schema/search_api_test_dependencies.schema.yml
@@ -0,0 +1,9 @@
+search_api.datasource.plugin.search_api_test_dependencies:
+  type: config_dependencies
+
+plugin.plugin_configuration.search_api_processor.search_api_test_dependencies:
+  type: config_dependencies
+
+plugin.plugin_configuration.search_api_tracker.search_api_test_dependencies:
+  type: config_dependencies
+
diff --git a/tests/search_api_test_dependencies/search_api_test_dependencies.info.yml b/tests/search_api_test_dependencies/search_api_test_dependencies.info.yml
new file mode 100644
index 0000000..db94294
--- /dev/null
+++ b/tests/search_api_test_dependencies/search_api_test_dependencies.info.yml
@@ -0,0 +1,8 @@
+name: 'Search API Test Dependencies'
+type: module
+description: 'Support module for Search API tests'
+package: Search
+dependencies:
+  - search_api:search_api
+core: 8.x
+hidden: true
diff --git a/tests/search_api_test_dependencies/src/Plugin/search_api/datasource/TestDatasource.php b/tests/search_api_test_dependencies/src/Plugin/search_api/datasource/TestDatasource.php
new file mode 100644
index 0000000..800f18f
--- /dev/null
+++ b/tests/search_api_test_dependencies/src/Plugin/search_api/datasource/TestDatasource.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search_api_test_dependencies\Plugin\search_api\datasource\TestDatasource.
+ */
+
+namespace Drupal\search_api_test_dependencies\Plugin\search_api\datasource;
+
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\search_api\Datasource\DatasourcePluginBase;
+
+/**
+ * @SearchApiDatasource(
+ *   id = "search_api_test_dependencies",
+ *   label = @Translation("Dependency test datasource"),
+ * )
+ */
+class TestDatasource extends DatasourcePluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemId(ComplexDataInterface $item) {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onDependencyRemoval(array $dependencies) {
+    $remove = \Drupal::state()
+      ->get('search_api_test_dependencies.datasource.remove', FALSE);
+    if ($remove) {
+      $this->configuration = array();
+    }
+    return $remove;
+  }
+
+}
diff --git a/tests/search_api_test_dependencies/src/Plugin/search_api/processor/TestProcessor.php b/tests/search_api_test_dependencies/src/Plugin/search_api/processor/TestProcessor.php
new file mode 100644
index 0000000..5ce230c
--- /dev/null
+++ b/tests/search_api_test_dependencies/src/Plugin/search_api/processor/TestProcessor.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search_api_test_dependencies\Plugin\search_api\processor\TestProcessor.
+ */
+
+namespace Drupal\search_api_test_dependencies\Plugin\search_api\processor;
+
+use Drupal\search_api\Processor\ProcessorPluginBase;
+
+/**
+ * @SearchApiProcessor(
+ *   id = "search_api_test_dependencies",
+ *   label = @Translation("Dependency test processor"),
+ * )
+ */
+class TestProcessor extends ProcessorPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onDependencyRemoval(array $dependencies) {
+    $remove = \Drupal::state()
+      ->get('search_api_test_dependencies.processor.remove', FALSE);
+    if ($remove) {
+      $this->configuration = array();
+    }
+    return $remove;
+  }
+
+}
diff --git a/tests/search_api_test_dependencies/src/Plugin/search_api/tracker/TestTracker.php b/tests/search_api_test_dependencies/src/Plugin/search_api/tracker/TestTracker.php
new file mode 100644
index 0000000..6ac77d4
--- /dev/null
+++ b/tests/search_api_test_dependencies/src/Plugin/search_api/tracker/TestTracker.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search_api_test_dependencies\Plugin\search_api\tracker\TestTracker.
+ */
+
+namespace Drupal\search_api_test_dependencies\Plugin\search_api\tracker;
+
+use Drupal\search_api\Plugin\search_api\tracker\Basic as BasicTracker;
+
+/**
+ * @SearchApiTracker(
+ *   id = "search_api_test_dependencies",
+ *   label = @Translation("Test tracker"),
+ * )
+ */
+class TestTracker extends BasicTracker {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onDependencyRemoval(array $dependencies) {
+    $remove = \Drupal::state()
+      ->get('search_api_test_dependencies.tracker.remove', FALSE);
+    if ($remove) {
+      $this->configuration = array();
+    }
+    return $remove;
+  }
+
+}
diff --git a/tests/src/Kernel/DependencyRemovalTest.php b/tests/src/Kernel/DependencyRemovalTest.php
new file mode 100644
index 0000000..7aa0cc3
--- /dev/null
+++ b/tests/src/Kernel/DependencyRemovalTest.php
@@ -0,0 +1,409 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\search_api\Kernel\DependencyRemovalTest.
+ */
+
+namespace Drupal\Tests\search_api\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\search_api\Entity\Index;
+use Drupal\search_api\Entity\Server;
+
+/**
+ * Tests what happens when an index's dependencies are removed.
+ *
+ * @group search_api
+ */
+class DependencyRemovalTest extends KernelTestBase {
+
+  /**
+   * A search index.
+   *
+   * @var \Drupal\search_api\IndexInterface
+   */
+  protected $index;
+
+  /**
+   * A config entity, to be used as a dependency in the tests.
+   *
+   * @var \Drupal\Core\Config\Entity\ConfigEntityInterface
+   */
+  protected $dependency;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = array(
+    'user',
+    'search_api',
+    'search_api_test_backend',
+    'search_api_test_dependencies',
+  );
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // The server tasks manager is needed when removing a server.
+    $mock = $this->getMock('Drupal\search_api\Task\ServerTaskManagerInterface');
+    $this->container->set('search_api.server_task_manager', $mock);
+
+    // Create the index object, but don't save it yet since we want to change
+    // its settings anyways in every test.
+    $this->index = Index::create(array(
+      'id' => 'test_index',
+      'name' => 'Test index',
+      'tracker' => 'default',
+      'datasources' => array(
+        'entity:user',
+      ),
+    ));
+
+    // Use a search server as the dependency, since we have that available
+    // anyways. The entity type should not matter at all, though.
+    $this->dependency = Server::create(array(
+      'id' => 'dependency',
+      'name' => 'Test dependency',
+      'backend' => 'search_api_test_backend',
+    ));
+    $this->dependency->save();
+  }
+
+  /**
+   * Tests a backend with a dependency that gets removed.
+   *
+   * If the dependency does not get removed, proper cascading to the index is
+   * also verified.
+   *
+   * @param bool $remove_dependency
+   *   Whether to remove the dependency from the backend when the object
+   *   depended on is deleted.
+   *
+   * @dataProvider dependencyTestDataProvider
+   */
+  public function testBackendDependency($remove_dependency) {
+    $dependency_key = $this->dependency->getConfigDependencyKey();
+    $dependency_name = $this->dependency->getConfigDependencyName();
+
+    // Create a server using the test backend, and set the dependency in the
+    // configuration.
+    /** @var \Drupal\search_api\ServerInterface $server */
+    $server = Server::create(array(
+      'id' => 'test_server',
+      'name' => 'Test server',
+      'backend' => 'search_api_test_backend',
+      'backend_config' => array(
+        'dependencies' => array(
+          $dependency_key => array(
+            $dependency_name,
+          ),
+        ),
+      ),
+    ));
+    $server->save();
+    $server_dependency_key = $server->getConfigDependencyKey();
+    $server_dependency_name = $server->getConfigDependencyName();
+
+    // Set the server on the index and save that, too. However, we don't want
+    // the index enabled, since that would lead to all kinds of overhead which
+    // is completely irrelevant for this test.
+    $this->index->set('server', $server->id());
+    $this->index->disable();
+    $this->index->save();
+
+    // Check the dependencies were calculated correctly.
+    $server_dependencies = $server->getDependencies();
+    $this->assertContains($dependency_name, $server_dependencies[$dependency_key], 'Backend dependency correctly inserted');
+    $index_dependencies = $this->index->getDependencies();
+    $this->assertContains($server_dependency_name, $index_dependencies[$server_dependency_key], 'Server dependency correctly inserted');
+
+    // Set our magic state key to let the test plugin know whether the
+    // dependency should be removed or not. See
+    // \Drupal\search_api_test_backend\Plugin\search_api\backend\TestBackend::onDependencyRemoval().
+    $key = 'search_api_test_backend.dependencies.remove';
+    \Drupal::state()->set($key, $remove_dependency);
+
+    // Delete the backend's dependency.
+    $this->dependency->delete();
+
+    // Reload the index and check it's still there.
+    $this->reloadIndex();
+    $this->assertNotNull($this->index, 'Index not removed');
+
+    // Reload the server.
+    $storage = \Drupal::entityTypeManager()->getStorage('search_api_server');
+    $storage->resetCache();
+    $server = $storage->load($server->id());
+
+    if ($remove_dependency) {
+      $this->assertNotNull($server, 'Server was not removed');
+      $this->assertArrayNotHasKey('dependencies', $server->get('backend_config'), 'Backend config was adapted');
+      // @todo Logically, this should not be changed: if the server does not get
+      //   removed, there is no need to adapt the index's configuration.
+      //   However, the way this config dependency cascading is actually
+      //   implemented in
+      //   \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval()
+      //   does not seem to follow that logic, but just computes the complete
+      //   tree of dependencies once and operates generally on the assumption
+      //   that all of them will be deleted. See #2642374.
+//      $this->assertEquals($server->id(), $this->index->getServerId(), "Index's server was not changed");
+    }
+    else {
+      $this->assertNull($server, 'Server was removed');
+      $this->assertEquals(NULL, $this->index->getServerId(), 'Index server was changed');
+    }
+  }
+
+  /**
+   * Tests a datasource with a dependency that gets removed.
+   *
+   * @param bool $remove_dependency
+   *   Whether to remove the dependency from the datasource when the object
+   *   depended on is deleted.
+   *
+   * @dataProvider dependencyTestDataProvider
+   */
+  public function testDatasourceDependency($remove_dependency) {
+    // Add the datasource to the index and save it. The datasource configuration
+    // contains the dependencies it will return – in our case, we use the test
+    // server.
+    $dependency_key = $this->dependency->getConfigDependencyKey();
+    $dependency_name = $this->dependency->getConfigDependencyName();
+    $this->index->set('datasources', array(
+      'entity:user',
+      'search_api_test_dependencies',
+    ));
+    $this->index->set('datasource_configs', array(
+      'search_api_test_dependencies' => array(
+        $dependency_key => array(
+          $dependency_name,
+        ),
+      ),
+    ));
+    $this->index->save();
+
+    // Check the dependencies were calculated correctly.
+    $dependencies = $this->index->getDependencies();
+    $this->assertContains($dependency_name, $dependencies[$dependency_key], 'Datasource dependency correctly inserted');
+
+    // Set our magic state key to let the test plugin know whether the
+    // dependency should be removed or not. See
+    // \Drupal\search_api_test_dependencies\Plugin\search_api\datasource\TestDatasource::onDependencyRemoval().
+    $key = 'search_api_test_dependencies.datasource.remove';
+    \Drupal::state()->set($key, $remove_dependency);
+
+    // Delete the datasource's dependency.
+    $this->dependency->delete();
+
+    // Reload the index and check it's still there.
+    $this->reloadIndex();
+    $this->assertNotNull($this->index, 'Index not removed');
+
+    // Make sure the dependency has been removed, one way or the other.
+    $dependencies = $this->index->getDependencies();
+    $dependencies += array($dependency_key => array());
+    $this->assertNotContains($dependency_name, $dependencies[$dependency_key], 'Datasource dependency removed from index');
+
+    // Depending on whether the plugin should have removed the dependency or
+    // not, make sure the right action was taken.
+    $datasources = $this->index->get('datasources');
+    $datasource_configs = $this->index->get('datasource_configs');
+    if ($remove_dependency) {
+      $this->assertContains('search_api_test_dependencies', $datasources, 'Datasource not removed');
+      $this->assertEmpty($datasource_configs['search_api_test_dependencies'], 'Datasource settings adapted');
+    }
+    else {
+      $this->assertNotContains('search_api_test_dependencies', $datasources, 'Datasource removed');
+      $this->assertArrayNotHasKey('search_api_test_dependencies', $datasource_configs, 'Datasource config removed');
+    }
+  }
+
+  /**
+   * Tests removing the (hard) dependency of the index's single datasource.
+   */
+  public function testSingleDatasourceDependency() {
+    // Add the datasource to the index and save it. The datasource configuration
+    // contains the dependencies it will return – in our case, we use the test
+    // server.
+    $dependency_key = $this->dependency->getConfigDependencyKey();
+    $dependency_name = $this->dependency->getConfigDependencyName();
+    $this->index->set('datasources', array(
+      'search_api_test_dependencies',
+    ));
+    $this->index->set('datasource_configs', array(
+      'search_api_test_dependencies' => array(
+        $dependency_key => array(
+          $dependency_name,
+        ),
+      ),
+    ));
+    $this->index->save();
+
+    // Since in this test the index will be removed, we need a mock
+    $mock = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface');
+    $mock_factory = $this->getMock('Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface');
+    $mock_factory->method('get')->willReturn($mock);
+    $this->container->set('keyvalue.expirable', $mock_factory);
+
+    // Delete the datasource's dependency.
+    $this->dependency->delete();
+
+    // Reload the index to ensure it was deleted.
+    $this->reloadIndex();
+    $this->assertNull($this->index, 'Index was removed');
+  }
+
+  /**
+   * Tests a processor with a dependency that gets removed.
+   *
+   * @param bool $remove_dependency
+   *   Whether to remove the dependency from the processor when the object
+   *   depended on is deleted.
+   *
+   * @dataProvider dependencyTestDataProvider
+   */
+  public function testProcessorDependency($remove_dependency) {
+    // Add the processor to the index and save it. The processor configuration
+    // contains the dependencies it will return – in our case, we use the test
+    // server.
+    $dependency_key = $this->dependency->getConfigDependencyKey();
+    $dependency_name = $this->dependency->getConfigDependencyName();
+    $this->index->set('processors', array(
+      'search_api_test_dependencies' => array(
+        'processor_id' => 'search_api_test_dependencies',
+        'settings' => array(
+          $dependency_key => array(
+            $dependency_name,
+          ),
+        ),
+      ),
+    ));
+    $this->index->save();
+
+    // Check the dependencies were calculated correctly.
+    $dependencies = $this->index->getDependencies();
+    $this->assertContains($dependency_name, $dependencies[$dependency_key], 'Processor dependency correctly inserted');
+
+    // Set our magic state key to let the test plugin know whether the
+    // dependency should be removed or not. See
+    // \Drupal\search_api_test_dependencies\Plugin\search_api\processor\TestProcessor::onDependencyRemoval().
+    $key = 'search_api_test_dependencies.processor.remove';
+    \Drupal::state()->set($key, $remove_dependency);
+
+    // Delete the processor's dependency.
+    $this->dependency->delete();
+
+    // Reload the index and check it's still there.
+    $this->reloadIndex();
+    $this->assertNotNull($this->index, 'Index not removed');
+
+    // Make sure the dependency has been removed, one way or the other.
+    $dependencies = $this->index->getDependencies();
+    $dependencies += array($dependency_key => array());
+    $this->assertNotContains($dependency_name, $dependencies[$dependency_key], 'Processor dependency removed from index');
+
+    // Depending on whether the plugin should have removed the dependency or
+    // not, make sure the right action was taken.
+    $processors = $this->index->get('processors');
+    if ($remove_dependency) {
+      $this->assertArrayHasKey('search_api_test_dependencies', $processors, 'Processor not removed');
+      $this->assertEmpty($processors['search_api_test_dependencies']['settings'], 'Processor settings adapted');
+    }
+    else {
+      $this->assertArrayNotHasKey('search_api_test_dependencies', $processors, 'Processor removed');
+    }
+  }
+
+  /**
+   * Tests a tracker with a dependency that gets removed.
+   *
+   * @param bool $remove_dependency
+   *   Whether to remove the dependency from the tracker when the object
+   *   depended on is deleted.
+   *
+   * @dataProvider dependencyTestDataProvider
+   */
+  public function testTrackerDependency($remove_dependency) {
+    // Set the tracker for the index and save it. The tracker configuration
+    // contains the dependencies it will return – in our case, we use the test
+    // server.
+    $dependency_key = $this->dependency->getConfigDependencyKey();
+    $dependency_name = $this->dependency->getConfigDependencyName();
+    $this->index->set('tracker', 'search_api_test_dependencies');
+    $this->index->set('tracker_config', array(
+      $dependency_key => array(
+        $dependency_name,
+      ),
+    ));
+    $this->index->save();
+
+    // Check the dependencies were calculated correctly.
+    $dependencies = $this->index->getDependencies();
+    $this->assertContains($dependency_name, $dependencies[$dependency_key], 'Tracker dependency correctly inserted');
+
+    // Set our magic state key to let the test plugin know whether the
+    // dependency should be removed or not. See
+    // \Drupal\search_api_test_dependencies\Plugin\search_api\tracker\TestTracker::onDependencyRemoval().
+    $key = 'search_api_test_dependencies.tracker.remove';
+    \Drupal::state()->set($key, $remove_dependency);
+    // If the index resets the tracker, it needs to have the config setting to
+    // work correctly.
+    if (!$remove_dependency) {
+      \Drupal::configFactory()->getEditable('search_api.settings')
+        ->set('default_tracker', 'default')
+        ->save();
+    }
+
+    // Delete the tracker's dependency.
+    $this->dependency->delete();
+
+    // Reload the index and check it's still there.
+    $this->reloadIndex();
+    $this->assertNotNull($this->index, 'Index not removed');
+
+    // Make sure the dependency has been removed, one way or the other.
+    $dependencies = $this->index->getDependencies();
+    $dependencies += array($dependency_key => array());
+    $this->assertNotContains($dependency_name, $dependencies[$dependency_key], 'Tracker dependency removed from index');
+
+    // Depending on whether the plugin should have removed the dependency or
+    // not, make sure the right action was taken.
+    $tracker = $this->index->get('tracker');
+    $tracker_config = $this->index->get('tracker_config');
+    if ($remove_dependency) {
+      $this->assertEquals('search_api_test_dependencies', $tracker, 'Tracker not reset');
+      $this->assertEmpty($tracker_config, 'Tracker settings adapted');
+    }
+    else {
+      $this->assertEquals('default', $tracker, 'Tracker was reset');
+      $this->assertEmpty($tracker_config, 'Tracker settings were cleared');
+    }
+  }
+
+  /**
+   * Data provider for this class's test methods.
+   *
+   * @return array
+   *   An array of argument arrays for this class's test methods.
+   */
+  public function dependencyTestDataProvider() {
+    return array(
+      array(TRUE),
+      array(FALSE),
+    );
+  }
+
+  /**
+   * Reloads the index with the latest copy from storage.
+   */
+  protected function reloadIndex() {
+    $storage = \Drupal::entityTypeManager()->getStorage('search_api_index');
+    $storage->resetCache();
+    $this->index = $storage->load($this->index->id());
+  }
+
+}
