diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index 84f17872759adcc77c520ef195e8cd8ecb4f2541..c5487a34cb7624fd0a8dcc143eebf71856bf6dc1 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -4,7 +4,7 @@ services:
     arguments: ['@keyvalue.expirable', '@datetime.time', 24]
   automatic_updates.updater:
     class: Drupal\automatic_updates\Updater
-    arguments: ['@state', '@string_translation','@automatic_updates.beginner', '@automatic_updates.stager', '@automatic_updates.cleaner', '@automatic_updates.committer', '@event_dispatcher', '@config.factory']
+    arguments: ['@state', '@string_translation','@automatic_updates.beginner', '@automatic_updates.stager', '@automatic_updates.cleaner', '@automatic_updates.committer', '@event_dispatcher', '@automatic_updates.path_locator']
   automatic_updates.beginner:
     class: PhpTuf\ComposerStager\Domain\Beginner
     arguments:
@@ -73,7 +73,7 @@ services:
       - { name: event_subscriber }
   automatic_updates.staged_projects_validator:
     class: Drupal\automatic_updates\Validator\StagedProjectsValidator
-    arguments: [ '@string_translation', '@automatic_updates.updater' ]
+    arguments: ['@string_translation', '@automatic_updates.path_locator']
     tags:
       - { name: event_subscriber }
   automatic_updates.update_version_validator:
@@ -86,3 +86,7 @@ services:
     arguments: ['@automatic_updates.exec_finder']
     tags:
       - { name: event_subscriber }
+  automatic_updates.path_locator:
+    class: Drupal\automatic_updates\PathLocator
+    arguments:
+      - '@config.factory'
diff --git a/src/PathLocator.php b/src/PathLocator.php
new file mode 100644
index 0000000000000000000000000000000000000000..ed1bdad7843ad7306b7ce658d0e0708889b439ac
--- /dev/null
+++ b/src/PathLocator.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\automatic_updates;
+
+use Composer\Autoload\ClassLoader;
+use Drupal\Component\FileSystem\FileSystem;
+use Drupal\Core\Config\ConfigFactoryInterface;
+
+/**
+ * Computes file system paths that are needed for automatic updates.
+ */
+class PathLocator {
+
+  /**
+   * The config factory service.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * Constructs a PathLocator object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory service.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory) {
+    $this->configFactory = $config_factory;
+  }
+
+  /**
+   * Returns the path of the active directory, which should be updated.
+   *
+   * @return string
+   *   The absolute path which should be updated.
+   */
+  public function getActiveDirectory(): string {
+    return $this->getProjectRoot();
+  }
+
+  /**
+   * Returns the path of the directory where updates should be staged.
+   *
+   * @return string
+   *   The absolute path of the directory where updates should be staged.
+   */
+  public function getStageDirectory(): string {
+    // Append the site ID to the directory in order to support parallel test
+    // runs, or multiple sites hosted on the same server.
+    $site_id = $this->configFactory->get('system.site')->get('uuid');
+    return FileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR . '.automatic_updates_stage_' . $site_id;
+  }
+
+  /**
+   * Returns the absolute path of the project root.
+   *
+   * This is where the project-level composer.json should normally be found, and
+   * may or may not be the same path as the Drupal code base.
+   *
+   * @return string
+   *   The absolute path of the project root.
+   */
+  public function getProjectRoot(): string {
+    // Assume that the vendor directory is immediately below the project root.
+    return realpath($this->getVendorDirectory() . DIRECTORY_SEPARATOR . '..');
+  }
+
+  /**
+   * Returns the absolute path of the vendor directory.
+   *
+   * @return string
+   *   The absolute path of the vendor directory.
+   */
+  public function getVendorDirectory(): string {
+    $reflector = new \ReflectionClass(ClassLoader::class);
+    return dirname($reflector->getFileName(), 2);
+  }
+
+}
diff --git a/src/Updater.php b/src/Updater.php
index ca8d2c1c868bcf896d2ef43181946bfbe7874c1e..0624bf8173e8fc55d39d41f5167b2d464398f95a 100644
--- a/src/Updater.php
+++ b/src/Updater.php
@@ -2,14 +2,11 @@
 
 namespace Drupal\automatic_updates;
 
-use Composer\Autoload\ClassLoader;
 use Drupal\automatic_updates\Event\PreCommitEvent;
 use Drupal\automatic_updates\Event\PreStartEvent;
 use Drupal\automatic_updates\Event\UpdateEvent;
 use Drupal\automatic_updates\Exception\UpdateException;
-use Drupal\Component\FileSystem\FileSystem;
 use Drupal\Component\Serialization\Json;
-use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\State\StateInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
@@ -77,11 +74,11 @@ class Updater {
   protected $eventDispatcher;
 
   /**
-   * The config factory service.
+   * The path locator service.
    *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   * @var \Drupal\automatic_updates\PathLocator
    */
-  protected $configFactory;
+  protected $pathLocator;
 
   /**
    * Constructs an Updater object.
@@ -100,10 +97,10 @@ class Updater {
    *   The Composer Stager's committer service.
    * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
    *   The event dispatcher service.
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The config factory service.
+   * @param \Drupal\automatic_updates\PathLocator $path_locator
+   *   The path locator service.
    */
-  public function __construct(StateInterface $state, TranslationInterface $translation, BeginnerInterface $beginner, StagerInterface $stager, CleanerInterface $cleaner, CommitterInterface $committer, EventDispatcherInterface $event_dispatcher, ConfigFactoryInterface $config_factory) {
+  public function __construct(StateInterface $state, TranslationInterface $translation, BeginnerInterface $beginner, StagerInterface $stager, CleanerInterface $cleaner, CommitterInterface $committer, EventDispatcherInterface $event_dispatcher, PathLocator $path_locator) {
     $this->state = $state;
     $this->beginner = $beginner;
     $this->stager = $stager;
@@ -111,31 +108,7 @@ class Updater {
     $this->committer = $committer;
     $this->setStringTranslation($translation);
     $this->eventDispatcher = $event_dispatcher;
-    $this->configFactory = $config_factory;
-  }
-
-  /**
-   * Gets the vendor directory.
-   *
-   * @return string
-   *   The absolute path for vendor directory.
-   */
-  private static function getVendorDirectory(): string {
-    $class_loader_reflection = new \ReflectionClass(ClassLoader::class);
-    return dirname($class_loader_reflection->getFileName(), 2);
-  }
-
-  /**
-   * Gets the stage directory.
-   *
-   * @return string
-   *   The absolute path for stage directory.
-   */
-  public function getStageDirectory(): string {
-    // Append the site ID to the directory in order to support parallel test
-    // runs, or multiple sites hosted on the same server.
-    $site_id = $this->configFactory->get('system.site')->get('uuid');
-    return FileSystem::getOsTemporaryDirectory() . '/.automatic_updates_stage_' . $site_id;
+    $this->pathLocator = $path_locator;
   }
 
   /**
@@ -145,23 +118,13 @@ class Updater {
    *   TRUE if there is active update, otherwise FALSE.
    */
   public function hasActiveUpdate(): bool {
-    $staged_dir = $this->getStageDirectory();
+    $staged_dir = $this->pathLocator->getStageDirectory();
     if (is_dir($staged_dir) || $this->state->get(static::STATE_KEY)) {
       return TRUE;
     }
     return FALSE;
   }
 
-  /**
-   * Gets the active directory.
-   *
-   * @return string
-   *   The absolute path for active directory.
-   */
-  public function getActiveDirectory(): string {
-    return realpath(static::getVendorDirectory() . '/..');
-  }
-
   /**
    * Begins the update.
    *
@@ -184,7 +147,7 @@ class Updater {
     $stage_key = $this->createActiveStage($packages);
     /** @var \Drupal\automatic_updates\Event\PreStartEvent $event */
     $event = $this->dispatchUpdateEvent(new PreStartEvent($packages), AutomaticUpdatesEvents::PRE_START);
-    $this->beginner->begin(static::getActiveDirectory(), static::getStageDirectory(), $this->getExclusions($event));
+    $this->beginner->begin($this->pathLocator->getActiveDirectory(), $this->pathLocator->getStageDirectory(), $this->getExclusions($event));
     return $stage_key;
   }
 
@@ -210,7 +173,7 @@ class Updater {
    *   detecting the core package.
    */
   public function getCorePackageName(): string {
-    $composer = realpath(static::getVendorDirectory() . '/../composer.json');
+    $composer = realpath($this->pathLocator->getProjectRoot() . '/composer.json');
 
     if (empty($composer) || !file_exists($composer)) {
       throw new \RuntimeException("Could not find project-level composer.json");
@@ -239,7 +202,7 @@ class Updater {
    */
   private function getExclusions($event): array {
     $make_relative = function (string $path): string {
-      return str_replace($this->getActiveDirectory() . '/', '', $path);
+      return str_replace($this->pathLocator->getActiveDirectory() . '/', '', $path);
     };
     return array_map($make_relative, $event->getExcludedPaths());
   }
@@ -270,15 +233,16 @@ class Updater {
   public function commit(): void {
     /** @var \Drupal\automatic_updates\Event\PreCommitEvent $event */
     $event = $this->dispatchUpdateEvent(new PreCommitEvent(), AutomaticUpdatesEvents::PRE_COMMIT);
-    $this->committer->commit($this->getStageDirectory(), static::getActiveDirectory(), $this->getExclusions($event));
+    $this->committer->commit($this->pathLocator->getStageDirectory(), $this->pathLocator->getActiveDirectory(), $this->getExclusions($event));
   }
 
   /**
    * Cleans the current update.
    */
   public function clean(): void {
-    if (is_dir($this->getStageDirectory())) {
-      $this->cleaner->clean($this->getStageDirectory());
+    $stage_dir = $this->pathLocator->getStageDirectory();
+    if (is_dir($stage_dir)) {
+      $this->cleaner->clean($stage_dir);
     }
     $this->state->delete(static::STATE_KEY);
   }
@@ -293,7 +257,7 @@ class Updater {
    * @see \PhpTuf\ComposerStager\Domain\StagerInterface::stage()
    */
   protected function stageCommand(array $command): void {
-    $this->stager->stage($command, $this->getStageDirectory());
+    $this->stager->stage($command, $this->pathLocator->getStageDirectory());
   }
 
   /**
diff --git a/src/Validator/StagedProjectsValidator.php b/src/Validator/StagedProjectsValidator.php
index faf6f23752633048a2ceebec9d126884d06c5d5e..72fa7519cbfec3d209a498863aecce223b5c5133 100644
--- a/src/Validator/StagedProjectsValidator.php
+++ b/src/Validator/StagedProjectsValidator.php
@@ -5,7 +5,7 @@ namespace Drupal\automatic_updates\Validator;
 use Drupal\automatic_updates\AutomaticUpdatesEvents;
 use Drupal\automatic_updates\Event\UpdateEvent;
 use Drupal\automatic_updates\Exception\UpdateException;
-use Drupal\automatic_updates\Updater;
+use Drupal\automatic_updates\PathLocator;
 use Drupal\automatic_updates\Validation\ValidationResult;
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -20,23 +20,23 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
   use StringTranslationTrait;
 
   /**
-   * The updater service.
+   * The path locator service.
    *
-   * @var \Drupal\automatic_updates\Updater
+   * @var \Drupal\automatic_updates\PathLocator
    */
-  protected $updater;
+  protected $pathLocator;
 
   /**
    * Constructs a StagedProjectsValidation object.
    *
    * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
    *   The translation service.
-   * @param \Drupal\automatic_updates\Updater $updater
-   *   The updater service.
+   * @param \Drupal\automatic_updates\PathLocator $path_locator
+   *   The path locator service.
    */
-  public function __construct(TranslationInterface $translation, Updater $updater) {
+  public function __construct(TranslationInterface $translation, PathLocator $path_locator) {
     $this->setStringTranslation($translation);
-    $this->updater = $updater;
+    $this->pathLocator = $path_locator;
   }
 
   /**
@@ -87,8 +87,8 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
    */
   public function validateStagedProjects(UpdateEvent $event): void {
     try {
-      $active_packages = $this->getDrupalPackagesFromLockFile($this->updater->getActiveDirectory() . "/composer.lock");
-      $staged_packages = $this->getDrupalPackagesFromLockFile($this->updater->getStageDirectory() . "/composer.lock");
+      $active_packages = $this->getDrupalPackagesFromLockFile($this->pathLocator->getActiveDirectory() . "/composer.lock");
+      $staged_packages = $this->getDrupalPackagesFromLockFile($this->pathLocator->getStageDirectory() . "/composer.lock");
     }
     catch (UpdateException $e) {
       foreach ($e->getValidationResults() as $result) {
diff --git a/tests/modules/automatic_updates_test/src/AutomaticUpdatesTestServiceProvider.php b/tests/modules/automatic_updates_test/src/AutomaticUpdatesTestServiceProvider.php
deleted file mode 100644
index b0bd0924c0852b2fafa466760518fd7388acdd3f..0000000000000000000000000000000000000000
--- a/tests/modules/automatic_updates_test/src/AutomaticUpdatesTestServiceProvider.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-namespace Drupal\automatic_updates_test;
-
-use Drupal\Core\DependencyInjection\ContainerBuilder;
-use Drupal\Core\DependencyInjection\ServiceProviderBase;
-
-/**
- * Modifies service definitions for testing purposes.
- */
-class AutomaticUpdatesTestServiceProvider extends ServiceProviderBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function alter(ContainerBuilder $container) {
-    parent::alter($container);
-
-    $service = 'automatic_updates.updater';
-    if ($container->hasDefinition($service)) {
-      $container->getDefinition($service)->setClass(TestUpdater::class);
-    }
-  }
-
-}
diff --git a/tests/modules/automatic_updates_test/src/TestUpdater.php b/tests/modules/automatic_updates_test/src/TestUpdater.php
deleted file mode 100644
index aed821d88aead5943ae088c313fcda4aa39d268c..0000000000000000000000000000000000000000
--- a/tests/modules/automatic_updates_test/src/TestUpdater.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-namespace Drupal\automatic_updates_test;
-
-use Drupal\automatic_updates\Updater;
-
-/**
- * A testing updater that allows arbitrary active and stage directories.
- */
-class TestUpdater extends Updater {
-
-  /**
-   * The active directory to use, if different from the default.
-   *
-   * @var string
-   */
-  public $activeDirectory;
-
-  /**
-   * The stage directory to use, if different from the default.
-   *
-   * @var string
-   */
-  public $stageDirectory;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getActiveDirectory(): string {
-    return $this->activeDirectory ?: parent::getActiveDirectory();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getStageDirectory(): string {
-    return $this->stageDirectory ?: parent::getStageDirectory();
-  }
-
-}
diff --git a/tests/src/Functional/ExclusionsTest.php b/tests/src/Functional/ExclusionsTest.php
index cf0a77990f850901f346bc6debfab259ec5b0b9e..d427dc69f24f0bc95112528b9293900a00c44cec 100644
--- a/tests/src/Functional/ExclusionsTest.php
+++ b/tests/src/Functional/ExclusionsTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\automatic_updates\Functional;
 
+use Drupal\automatic_updates\PathLocator;
+use Drupal\automatic_updates\Updater;
 use Drupal\Core\Site\Settings;
 use Drupal\Tests\BrowserTestBase;
 
@@ -30,10 +32,22 @@ class ExclusionsTest extends BrowserTestBase {
   public function testExclusions(): void {
     $stage_dir = "$this->siteDirectory/stage";
 
-    /** @var \Drupal\automatic_updates_test\TestUpdater $updater */
-    $updater = $this->container->get('automatic_updates.updater');
-    $updater->activeDirectory = __DIR__ . '/../../fixtures/fake-site';
-    $updater->stageDirectory = $stage_dir;
+    /** @var \Drupal\automatic_updates\PathLocator|\Prophecy\Prophecy\ObjectProphecy $locator */
+    $locator = $this->prophesize(PathLocator::class);
+    $locator->getActiveDirectory()->willReturn(__DIR__ . '/../../fixtures/fake-site');
+    $locator->getStageDirectory()->willReturn($stage_dir);
+    $locator->getProjectRoot()->willReturn($this->getDrupalRoot());
+
+    $updater = new Updater(
+      $this->container->get('state'),
+      $this->container->get('string_translation'),
+      $this->container->get('automatic_updates.beginner'),
+      $this->container->get('automatic_updates.stager'),
+      $this->container->get('automatic_updates.cleaner'),
+      $this->container->get('automatic_updates.committer'),
+      $this->container->get('event_dispatcher'),
+      $locator->reveal()
+    );
 
     $settings = Settings::getAll();
     $settings['file_public_path'] = 'files/public';
diff --git a/tests/src/Unit/StagedProjectsValidatorTest.php b/tests/src/Unit/StagedProjectsValidatorTest.php
index 026e48e2078dcb12d68144c4c63fe057c6632cc3..6180fd99410269a064dd7d3c288ce824cc1539f8 100644
--- a/tests/src/Unit/StagedProjectsValidatorTest.php
+++ b/tests/src/Unit/StagedProjectsValidatorTest.php
@@ -3,7 +3,7 @@
 namespace Drupal\Tests\automatic_updates\Unit;
 
 use Drupal\automatic_updates\Event\UpdateEvent;
-use Drupal\automatic_updates\Updater;
+use Drupal\automatic_updates\PathLocator;
 use Drupal\automatic_updates\Validator\StagedProjectsValidator;
 use Drupal\Component\FileSystem\FileSystem;
 use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
@@ -26,10 +26,10 @@ class StagedProjectsValidatorTest extends UnitTestCase {
     $active_dir = uniqid($prefix);
     $stage_dir = uniqid($prefix);
 
-    $updater = $this->prophesize(Updater::class);
-    $updater->getActiveDirectory()->willReturn($active_dir);
-    $updater->getStageDirectory()->willReturn($stage_dir);
-    $validator = new StagedProjectsValidator(new TestTranslationManager(), $updater->reveal());
+    $locator = $this->prophesize(PathLocator::class);
+    $locator->getActiveDirectory()->willReturn($active_dir);
+    $locator->getStageDirectory()->willReturn($stage_dir);
+    $validator = new StagedProjectsValidator(new TestTranslationManager(), $locator->reveal());
 
     $event = new UpdateEvent();
     $validator->validateStagedProjects($event);
@@ -56,13 +56,13 @@ class StagedProjectsValidatorTest extends UnitTestCase {
    * @covers ::validateStagedProjects
    */
   public function testErrors(string $fixtures_dir, string $expected_summary, array $expected_messages): void {
-    $updater = $this->prophesize(Updater::class);
+    $locator = $this->prophesize(PathLocator::class);
     $this->assertNotEmpty($fixtures_dir);
     $this->assertDirectoryExists($fixtures_dir);
 
-    $updater->getActiveDirectory()->willReturn("$fixtures_dir/active");
-    $updater->getStageDirectory()->willReturn("$fixtures_dir/staged");
-    $validator = new StagedProjectsValidator(new TestTranslationManager(), $updater->reveal());
+    $locator->getActiveDirectory()->willReturn("$fixtures_dir/active");
+    $locator->getStageDirectory()->willReturn("$fixtures_dir/staged");
+    $validator = new StagedProjectsValidator(new TestTranslationManager(), $locator->reveal());
     $event = new UpdateEvent();
     $validator->validateStagedProjects($event);
     $results = $event->getResults();
@@ -121,10 +121,10 @@ class StagedProjectsValidatorTest extends UnitTestCase {
    */
   public function testNoErrors(): void {
     $fixtures_dir = realpath(__DIR__ . '/../../fixtures/project_staged_validation/no_errors');
-    $updater = $this->prophesize(Updater::class);
-    $updater->getActiveDirectory()->willReturn("$fixtures_dir/active");
-    $updater->getStageDirectory()->willReturn("$fixtures_dir/staged");
-    $validator = new StagedProjectsValidator(new TestTranslationManager(), $updater->reveal());
+    $locator = $this->prophesize(PathLocator::class);
+    $locator->getActiveDirectory()->willReturn("$fixtures_dir/active");
+    $locator->getStageDirectory()->willReturn("$fixtures_dir/staged");
+    $validator = new StagedProjectsValidator(new TestTranslationManager(), $locator->reveal());
     $event = new UpdateEvent();
     $validator->validateStagedProjects($event);
     $results = $event->getResults();
@@ -137,10 +137,10 @@ class StagedProjectsValidatorTest extends UnitTestCase {
    */
   public function testNoLockFile(): void {
     $fixtures_dir = realpath(__DIR__ . '/../../fixtures/project_staged_validation/no_errors');
-    $updater = $this->prophesize(Updater::class);
-    $updater->getActiveDirectory()->willReturn("$fixtures_dir/active");
-    $updater->getStageDirectory()->willReturn("$fixtures_dir");
-    $validator = new StagedProjectsValidator(new TestTranslationManager(), $updater->reveal());
+    $locator = $this->prophesize(PathLocator::class);
+    $locator->getActiveDirectory()->willReturn("$fixtures_dir/active");
+    $locator->getStageDirectory()->willReturn("$fixtures_dir");
+    $validator = new StagedProjectsValidator(new TestTranslationManager(), $locator->reveal());
     $event = new UpdateEvent();
     $validator->validateStagedProjects($event);
     $results = $event->getResults();