🔴 The Error You're Seeing

Confirm this matches your console output. If it does, you're in the right place.

ERROR LOG// DOC-DERIVED — wording stable since JDK 9; JDK 8 used "Provider ... not found": Exception in thread "main" java.util.ServiceConfigurationError: com.devinhyderabad.export.Exporter: provider com.devinhyderabad.export.PdfExporter not found at java.base/java.util.ServiceLoader.fail(ServiceLoader.java:586) at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.nextProviderClass(ServiceLoader.java:1160) at com.devinhyderabad.app.ReportRunner.main(ReportRunner.java:12)

⚡ Quick Fix Works 80% of the time

Ensure the jar that DEFINES PdfExporter ships alongside the jar declaring the META-INF/services entry — in fat jars verify both class file and service file made it into the archive.

unzip -l app.jar | grep -E "PdfExporter.class|META-INF/services/com.devinhyderabad.export.Exporter" # BOTH lines must appear - missing the first = this exact error

🧠 Why this Happens

Tap to expand the deep technical explanation

ServiceLoader resolves an SPI in two independent steps. It first reads META-INF/services/<interface-FQCN>, a plain text file listing implementation class names — this succeeds because the text resource traveled with your API jar. Only when iteration reaches the entry does it attempt Class.forName on the listed name; if the implementing class’s bytecode never shipped (separate artifact excluded from the fat jar, module path missing its provides directive, or a rename outliving the config file), loading fails and ServiceLoader wraps the failure in ServiceConfigurationError at next() rather than at startup.

The HITEC City Parking Spot Analogy:

A restaurant menu lists "Chef’s Special" but the kitchen never hired that chef. Ordering goes fine until the plate must actually be cooked — then staff admit nobody back there can make it.

🔁 How to Reproduce Confirm this is your error

Create an SPI interface plus META-INF/services file naming an implementation, ship the interface jar WITHOUT the impl class, iterate ServiceLoader.load(Exporter.class).next(). DOC-DERIVED — deterministic lazy failure.

🛠️ Solutions (5 Ways to Fix)

Solution 1✓ Most common cause

Ship provider and declaration as one unit

👉 Use this whenever you control packaging — the simplest permanent cure.

Bundle the implementing class in the same artifact (or a guaranteed companion) as its services file. Shade plugins merge META-INF/services via ServiceResourceTransformer so aggregation cannot orphan either half.

<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> <!-- merges META-INF/services across all shaded jars -->
Solution 2

Audit the deployed archive for both halves

👉 Use this during diagnosis on any jar/war/docker image already built.

One grep proves which half went missing: the services text file, the provider .class, or both. Then trace why packaging dropped it — exclusion filters, minimizeJar stripping, Docker COPY globs.

unzip -l app.jar | grep -E "PdfExporter|services/.*Exporter" # shade minimizeJar often strips providers referenced only reflectively: <minimizeJar>false</minimizeJar>
Solution 3

Declare provides directives on the module path

👉 Use this for JPMS deployments where META-INF alone no longer registers providers.

In named modules ServiceLoader consults module declarations; a module exporting the SPI but never providing implementations yields exactly this error at iteration. Add provides...with to the implementing module.

module devtoolpick.export.pdf { requires devtoolpick.export.api; provides com.devinhyderabad.export.Exporter with com.devinhyderabad.export.PdfExporter; }
Solution 4

Keep the services file synchronized with refactoring

👉 Use this after package renames or class moves inside the provider project.

The text file names classes verbatim; IDE refactors never touch it. Add a unit test that loads every declared provider and fails on drift, converting silent runtime rot into build-time failure.

@Test void everyDeclaredProviderLoads() { assertDoesNotThrow(() -> ServiceLoader.load(Exporter.class).forEach(p -> assertNotNull(p))); }
Solution 5

Catch the error and continue with remaining providers

👉 DEV ONLY — masks wiring bugs; see note.

DEV ONLY. Iterating with try/catch around next() lets partial functionality survive a broken provider, but it hides packaging defects until users notice missing features. Reserve for plugin hosts that genuinely tolerate absent extensions — and log loudly.

// DEV ONLY - tolerant plugin host pattern Iterator<Exporter> it = ServiceLoader.load(Exporter.class).iterator(); while (it.hasNext()) { try { register(it.next()); } catch (ServiceConfigurationError e) { e.printStackTrace(); /* keep going */ } }

📋 Version Notes

Java 8

Message reads Provider com.x.Y not found; same lazy-failure semantics.

Java 11

Wording lowercase since 9; module-path resolution added.

Java 17

Unchanged; stream() API makes diagnostics easier.

Java 21

Unchanged.

🛡️ How to Prevent This Next Time

Test SPI loading in CI against the REAL packaged artifact (not exploded classes), merge service files correctly when shading, and declare provides/uses explicitly once any part of the deployment moves onto the module path.