diff --git a/src/IO/Directory.php b/src/IO/Directory.php
index 04ddd94..b6ffebb 100644
--- a/src/IO/Directory.php
+++ b/src/IO/Directory.php
@@ -1,78 +1,89 @@
 <?php
 
 namespace Keruald\OmniTools\IO;
 
 class Directory {
 
     ///
     /// Constructors
     ///
 
     public function __construct (
         private string $path,
     ) {}
 
     ///
     /// Getters and setters
     ///
 
     public function getPath () : string {
         return $this->path;
     }
 
     public function setPath (string $path) : self {
         $this->path = $path;
 
         return $this;
     }
 
     ///
     /// Directory properties methods
     ///
 
     public function exists () : bool {
         return is_dir($this->path);
     }
 
     public function isReadable () : bool {
         return is_readable($this->path);
     }
 
     public function isWritable () : bool {
         return is_writable($this->path);
     }
 
     /**
      * @return array<string, string>
      */
     public function getPathInfo () : array {
         return pathinfo($this->path);
     }
 
     public function getParentDirectory () : string {
         return pathinfo($this->path, PATHINFO_DIRNAME);
     }
 
     public function getDirectoryName () : string {
         return pathinfo($this->path, PATHINFO_BASENAME);
     }
 
     ///
     /// Search files
     ///
 
     /**
      * Gets files in the directory matching a specific pattern,
      * using the PHP glob function.
      *
      * @return File[]
      */
     public function glob (string $pattern) : array {
         return array_map(
             function ($file) {
                 return new File($file);
             }, glob("$this->path/$pattern")
         );
     }
 
+    /**
+     * @return Directory[]
+     */
+    public function getSubdirectories () : array {
+        return array_map(
+            function ($dir) {
+                return new Directory($dir);
+            }, glob("$this->path/*", GLOB_ONLYDIR)
+        );
+    }
+
 }
diff --git a/src/Registration/PSR4/PSR4Namespace.php b/src/Registration/PSR4/PSR4Namespace.php
new file mode 100644
index 0000000..dc096df
--- /dev/null
+++ b/src/Registration/PSR4/PSR4Namespace.php
@@ -0,0 +1,76 @@
+<?php
+declare(strict_types=1);
+
+namespace Keruald\OmniTools\Registration\PSR4;
+
+use Keruald\OmniTools\IO\Directory;
+use Keruald\OmniTools\IO\File;
+
+class PSR4Namespace {
+
+    public function __construct (
+        public string $namespacePrefix,
+        public string $baseDirectory,
+    ) {
+    }
+
+    ///
+    /// Auto-discovery
+    ///
+
+    /**
+     * Discover classes in the namespace folder following PSR-4 convention,
+     * directly at top-level, ignoring subdirectories.
+     *
+     * @see discoverRecursive
+     * @return string[]
+     */
+    public function discover () : array {
+        $files = (new Directory($this->baseDirectory))
+            ->glob("*.php");
+
+        return array_map(function (File $file) {
+            return $this->namespacePrefix
+                   . "\\" . $file->getFileNameWithoutExtension();
+        }, $files);
+    }
+
+    /**
+     * Discover classes in the namespace folder following PSR-4 convention,
+     * including all subfolders.
+     *
+     * @return string[]
+     */
+    public function discoverRecursive () : array {
+        $classes = $this->discover();
+
+        $subDirectories = (new Directory($this->baseDirectory))
+            ->getSubdirectories();
+
+        foreach ($subDirectories as $dir) {
+            $ns = new PSR4Namespace(
+                $this->namespacePrefix . "\\" . $dir->getDirectoryName(),
+                $dir->getPath(),
+            );
+
+            array_push($classes, ...$ns->discoverRecursive());
+        }
+
+        return $classes;
+    }
+
+    /**
+     * Discover classes for a specific namespace in a specific folder,
+     * following the PSR-4 convention, including all subfolders.
+     *
+     * @return string[]
+     */
+    public static function discoverAllClasses (
+        string $namespacePrefix,
+        string $baseDirectory
+    ) : array {
+        $ns = new PSR4Namespace($namespacePrefix, $baseDirectory);
+        return $ns->discoverRecursive();
+    }
+
+}
diff --git a/tests/Registration/PSR4/PSR4NamespaceTest.php b/tests/Registration/PSR4/PSR4NamespaceTest.php
new file mode 100644
index 0000000..93dffc5
--- /dev/null
+++ b/tests/Registration/PSR4/PSR4NamespaceTest.php
@@ -0,0 +1,84 @@
+<?php
+declare(strict_types=1);
+
+namespace Keruald\OmniTools\Tests\Registration\PSR4;
+
+use Keruald\OmniTools\Registration\PSR4\PSR4Namespace;
+
+use Keruald\OmniTools\Tests\WithData;
+use PHPUnit\Framework\TestCase;
+
+class PSR4NamespaceTest extends TestCase {
+
+    use WithData;
+
+    ///
+    /// Discovery tests
+    ///
+
+    const ALL_CLASSES = [
+        "Acme\\SolarSystemLib\\Sun",
+        "Acme\\SolarSystemLib\\Planets\\Pluton",
+        "Acme\\SolarSystemLib\\Planets\\Inner\\Mercure",
+        "Acme\\SolarSystemLib\\Planets\\Inner\\Venus",
+    ];
+
+    /**
+     * @dataProvider provideClasses
+     */
+    public function testDiscover (
+        string $path, string $prefix, array $expected
+    ) : void {
+        $ns = new PSR4Namespace($prefix, $this->getDataPath($path));
+
+        $this->assertEquals($expected, $ns->discover());
+    }
+
+    public function testDiscoverRecursive () : void {
+        $baseDirectory = $this->getDataPath("SolarSystemLib");
+        $ns = new PSR4Namespace("Acme\\SolarSystemLib", $baseDirectory);
+
+        $this->assertEquals(self::ALL_CLASSES, $ns->discoverRecursive());
+    }
+
+    public function testDiscoverAllClasses () : void {
+        $actual = PSR4Namespace::discoverAllClasses(
+            "Acme\\SolarSystemLib",
+            $this->getDataPath("SolarSystemLib"),
+        );
+
+        $this->assertEquals(self::ALL_CLASSES, $actual);
+
+    }
+
+    ///
+    /// Data providers
+    ///
+
+    public function provideClasses () : iterable {
+        // [string $path, string $prefix, string[] $expectedClasses]
+        yield ["MockLib", "Acme\\MockLib", [
+            "Acme\\MockLib\\Bar",
+            "Acme\\MockLib\\Foo",
+        ]];
+
+        yield ["SolarSystemLib", "Acme\\SolarSystemLib", [
+            "Acme\\SolarSystemLib\\Sun",
+        ]];
+
+        yield ["SolarSystemLib/Planets", "Acme\\SolarSystemLib\\Planets", [
+            "Acme\\SolarSystemLib\\Planets\\Pluton",
+        ]];
+
+        yield [
+            "SolarSystemLib/Planets/Inner",
+            "Acme\\SolarSystemLib\\Planets\\Inner",
+            [
+                "Acme\\SolarSystemLib\\Planets\\Inner\\Mercure",
+                "Acme\\SolarSystemLib\\Planets\\Inner\\Venus",
+            ]
+        ];
+
+        yield ["NotExisting", "AnyPrefix", []];
+    }
+}
diff --git a/tests/data/SolarSystemLib/Planets/Inner/Mercure.php b/tests/data/SolarSystemLib/Planets/Inner/Mercure.php
new file mode 100644
index 0000000..73763dd
--- /dev/null
+++ b/tests/data/SolarSystemLib/Planets/Inner/Mercure.php
@@ -0,0 +1,8 @@
+<?php
+declare(strict_types=1);
+
+namespace Acme\SolarSystemLib\Planets\Inner;
+
+class Mercure {
+
+}
diff --git a/tests/data/SolarSystemLib/Planets/Inner/Venus.php b/tests/data/SolarSystemLib/Planets/Inner/Venus.php
new file mode 100644
index 0000000..0546152
--- /dev/null
+++ b/tests/data/SolarSystemLib/Planets/Inner/Venus.php
@@ -0,0 +1,8 @@
+<?php
+declare(strict_types=1);
+
+namespace Acme\SolarSystemLib\Planets\Inner;
+
+class Venus {
+
+}
diff --git a/tests/data/SolarSystemLib/Planets/Pluton.php b/tests/data/SolarSystemLib/Planets/Pluton.php
new file mode 100644
index 0000000..5e7c5ec
--- /dev/null
+++ b/tests/data/SolarSystemLib/Planets/Pluton.php
@@ -0,0 +1,8 @@
+<?php
+declare(strict_types=1);
+
+namespace Acme\SolarSystemLib\Planets;
+
+class Pluton {
+
+}
diff --git a/tests/data/SolarSystemLib/Sun.php b/tests/data/SolarSystemLib/Sun.php
new file mode 100644
index 0000000..aa283f1
--- /dev/null
+++ b/tests/data/SolarSystemLib/Sun.php
@@ -0,0 +1,8 @@
+<?php
+declare(strict_types=1);
+
+namespace Acme\SolarSystemLib;
+
+class Sun {
+
+}