🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
// DOC-DERIVED — wording unchanged for decades; modifier list varies (method/class/field):
Exception in thread "main" java.lang.IllegalAccessError: tried to access method void com.devinhyderabad.core.BaseService.start()V from class com.devinhyderabad.web.PluginServlet
at com.devinhyderabad.web.PluginServlet.init(PluginServlet.java:18)
at com.devinhyderabad.app.Main.boot(Main.java:12)
at com.devinhyderabad.app.Main.main(Main.java:5)⚡ Quick Fix Works 80% of the time
Align the two artifacts to one version: dependency:tree both sides, exclude the stale core jar, and re-deploy as a single consistent unit.
mvn -q dependency:tree -Dincludes=com.devinhyderabad:core
mvn -q dependency:tree -Dincludes=com.devinhyderabad:web-plugin
# both must resolve core to the SAME version - pin it centrally🧠 Why this Happens
Tap to expand the deep technical explanation
Access control is enforced at linkage time from the modifiers present in the loaded binary, not remembered from compilation. When BaseService 1.x declared start() public and thousands of PluginServlet.class files baked invokes into it, everything worked. Core 2.0 narrowed start() to package-private as cleanup; the old servlet binary now requests a member the loaded class no longer grants, and the JVM throws IllegalAccessError at first resolution — even though source-level compilation of both together would have been impossible and caught it instantly.
The HITEC City Parking Spot Analogy:
Your keycard opens the server room today; tomorrow facilities recodes the door for the infrastructure team only. Swipe again and the lock simply blinks red — your card predates the change.
🔁 How to Reproduce Confirm this is your error
Compile PluginServlet against BaseService v1 where start() is public. Rebuild only BaseService with start() package-private and deploy the mixed pair. First invocation of start throws the captured wording. DOC-DERIVED — message format frozen since Java 1.0 era.
🛠️ Solutions (5 Ways to Fix)
Pin both artifacts to one version and redeploy atomically
👉 Use this first — mixed-version deploys are overwhelmingly the cause.
The compiler would reject the pairing; only deployment let it happen. Centralize versions via BOM or Gradle platform so web-plugin and core can never drift, then ship one artifact.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.devinhyderabad</groupId>
<artifactId>core</artifactId>
<version>2.3.1</version>
</dependency>
</dependencies>
</dependencyManagement>Never shrink visibility in released APIs — widen back and deprecate
👉 Use this when you publish the library that changed and consumers exist outside your build.
Binary compatibility requires access modifiers only ever widen within a major line. Restore the original visibility immediately, mark @Deprecated(since="2.0"), and schedule removal for the next major so callers migrate consciously.
/** @deprecated use {@link #begin()} - removal in 3.0 */
@Deprecated(since = "2.0")
public void start() { begin(); }
public void begin() { /* new canonical entry point */ }Check for split packages merging members across jars
👉 Use this when neither jar changed but both define classes in the same package.
Two jars contributing com.devinhyderabad.core means package-private members of one copy are invisible to classes resolved from the other copy — same error, zero version drift. Keep one owner per package or shade-relocate the intruder.
unzip -l app.jar | grep "com/devinhyderabad/core/" | sort
# classes listed from TWO different nested jars = split packageProvide an official facade instead of leaking internals
👉 Use this when external code genuinely needed the hidden operation.
If plugin ecosystems depended on start(), hiding it breaks them by design. Expose an intentional extension point (interface + registry) so third parties bind to contract, not to your class hierarchy internals.
public interface LifecycleHook { void onStart(BaseService svc); }
BaseService.registerLifecycle(new LifecycleHook() {
public void onStart(BaseService svc) { svc.begin(); }
});Force access reflectively at runtime
👉 DEV ONLY — never ship this; see below.
DEV ONLY. setAccessible(true) bypasses the check but couples you to JVM module policy (JPMS blocks it for non-opened packages), breaks under SecurityManager-style hardening, and documents nothing. Legitimate only inside test tooling like Mockito, never in product code reaching a demoted member.
// DEV ONLY - test tooling pattern, forbidden in production paths
Method m = BaseService.class.getDeclaredMethod("start");
m.setAccessible(true);
m.invoke(service);📋 Version Notes
Message format unchanged since inception; no module awareness.
Same on classpath; module reads add separate failure mode (see module-export sibling).
Same wording; reflective bypass attempts now collide with JPMS opens.
Unchanged.
🛡️ How to Prevent This Next Time
Run a binary-compatibility checker (Revapi/japicmp) on every release of published libraries, ban duplicate packages across artifacts, and deploy applications as single immutable bundles rather than layered directory drops.