{"skill":{"slug":"java-circular-dependency-breaker","displayName":"java-circular-dependency-breaker","summary":"Break circular dependencies in Java multi-module Gradle/Maven projects using interface extraction and business service separation. Triggers: 'circular depend...","description":"---\nname: java-circular-dependency-breaker\ndescription: \"Break circular dependencies in Java multi-module Gradle/Maven projects using interface extraction and business service separation. Triggers: 'circular dependency', '循环依赖', 'move service to another module', '模块循环依赖'.\"\norigin: project\n---\n\n# Java Circular Dependency Breaker\n\nTwo battle-tested patterns for breaking circular dependencies between Java multi-module projects (Gradle or Maven).\n\n## When to Activate\n\n- Gradle multi-module project with `api project(':module-a')` and `api project(':module-b')` forming a cycle, **or** Maven project with `<dependency><artifactId>module-a</artifactId></dependency>` forming a cycle\n- Refactoring a monolithic module into smaller modules without breaking existing code\n- Moving service classes from one module to another while callers still reference concrete implementations\n- A class has methods that depend on services from both modules, preventing direct migration\n\n## Design Pattern\n\nThis skill applies the **Mediator Pattern** — introducing an intermediary (interface or business service) to decouple modules that directly depend on each other. Instead of Module A referencing Module B's concrete class directly, the mediator sits between them, allowing both to evolve independently.\n\n- **Pattern 1** uses a service interface as the mediator — callers depend only on the abstraction.\n- **Pattern 2** uses a service layer as the mediator — it coordinates cross-module logic without duplicating data access, keeping the migrated base class pure.\n\n## Requirements\n\n- JDK 8+ (for `javap`)\n- Gradle or Maven build tool\n- Git (for rollback)\n\n## Before You Start\n\n> **Checkpoint 0** — Confirm preconditions before choosing a pattern:\n\n- [ ] Identify the concrete class causing the cycle\n  - Gradle: `./gradlew :module-a:dependencies --configuration compileClasspath | grep module-b`\n  - Maven: `mvn dependency:tree -pl module-a | grep module-b`\n- [ ] Confirm the class is referenced by **at least one external module** (otherwise just move it, no interface needed)\n- [ ] Compile all affected modules — **all tests must pass** before refactoring\n  - Gradle: `./gradlew compileJava compileTestJava`\n  - Maven: `mvn compile test-compile`\n- [ ] Read the Decision Tree below to choose the correct pattern\n\n**⚠️ Stop and ask the user if:**\n- The class has over 20 public methods (suggest splitting first)\n- The class is heavily mocked in tests across 5+ files (migration cost is high)\n- The module cycle involves 3+ modules (may need sequential application of patterns)\n\n## Pattern 1: Interface Extraction\n\nUse when: Module A references a concrete class in Module B, and the class has no cross-module method dependencies.\n\n### Prerequisites\n\n| Item | Description |\n|------|-------------|\n| **Input** | A concrete `@Service` / `@Component` class in Module B that is referenced by Module A |\n| **Constraint** | The class's public methods must NOT depend on services/classes from Module A |\n| **Time estimate** | 15–30 min for a class with ≤10 public methods |\n| **Risk** | Low — changes are mechanical and compiler-verified |\n\n### Steps\n\n1. **Create the interface** in a shared module (or new `*-interface` module)\n   - Extract all public methods from the concrete class\n   - Keep the same method signatures, parameter types, return types\n   - Name convention: `IXxxService` (or your project's interface naming convention)\n   \n   > **Checkpoint 1** — Before proceeding, verify:\n   > - [ ] Interface methods are a **strict subset** of the concrete class's public methods (no extras)\n   > - [ ] Return types and parameter types in the interface do **not** depend on the source module\n   > - [ ] Run `javap -public com.example.module.NotificationService` and diff against interface\n\n2. **Implement the interface** in the target module\n   - Add `implements IXxxService` to the concrete class\n   - Keep `@Service` / `@Component` annotations\n   \n   > **Checkpoint 2** — Confirm:\n   > - [ ] `./gradlew :module-b:compileJava` passes (or `mvn -pl module-b compile`)\n   > - [ ] Spring context loads without bean name conflicts (check `@Qualifier` if needed)\n\n3. **Update all callers** in the source module\n   - Change field declaration from concrete class to interface\n   - Update `import` from `com.example.module.Class` to `com.example.interface.IXxxService`\n   \n   > **Checkpoint 3** — Confirm:\n   > - [ ] Zero remaining imports of the concrete class in the source module (`grep -r \"import com.example.module.NotificationService\" module-a/src/` returns empty)\n   > - [ ] Test mocks updated: `@Mock BeanName` → `@Mock IBeanName` (do this **before** removing the gradle dependency)\n\n4. **Remove the dependency** from source module's build file\n   - Gradle (`build.gradle`): Delete `api project(':module-b')`, keep `api project(':module-interface')`\n   - Maven (`pom.xml`): Delete the `module-b` dependency `<dependency>` block\n   \n   > **Checkpoint 4** — Final verification:\n   > - [ ] `./gradlew :module-a:compileJava` or `mvn -pl module-a compile` passes\n   > - [ ] `./gradlew :module-a:compileTestJava` or `mvn -pl module-a test-compile` passes\n   > - [ ] `./gradlew :module-a:dependencies --configuration compileClasspath | grep module-b` or `mvn dependency:tree -pl module-a | grep module-b` returns **EMPTY**\n   \n   > **⚠️ If compilation fails:** Re-add the dependency (`build.gradle` or `pom.xml`), fix the error, then remove again. Do **not** leave the codebase in a broken state.\n\n### Example\n\n```java\n// BEFORE: OrderService depends on concrete NotificationService\n// module-a/.../OrderService.java\n@Service\npublic class OrderService {\n    private final NotificationService notificationService;  // concrete\n}\n\n// AFTER: Interface extracted\n// module-interface/.../INotificationService.java\npublic interface INotificationService {\n    void startProcess(String key);\n}\n\n// module-b/.../NotificationService.java\n@Service\npublic class NotificationService implements INotificationService { ... }\n\n// module-a/.../OrderService.java\n@Service\npublic class OrderService {\n    private final INotificationService notificationService;  // interface\n}\n```\n\n### Success Criteria\n\n| Criterion | Verification |\n|-----------|-------------|\n| Module A no longer depends on Module B | Gradle: `./gradlew :module-a:dependencies --configuration compileClasspath \\| grep module-b` returns empty; Maven: `mvn dependency:tree -pl module-a \\| grep module-b` returns empty |\n| All callers use interface type | `grep -r \"import com.example.module.NotificationService\" module-a/src/` returns empty |\n| Spring context loads | `./gradlew :module-a:compileTestJava` or `mvn -pl module-a test-compile` passes |\n| Tests pass | `./gradlew test` or `mvn test` on all affected modules passes |\n\n---\n\n## Pattern 2: Business Service Extraction\n\nUse when: A class has **some methods** that depend on services from the source module, preventing the entire class from migrating to the target module.\n\n### Prerequisites\n\n| Item | Description |\n|------|-------------|\n| **Input** | A class in the source module with mixed dependencies (some internal, some cross-module) |\n| **Constraint** | The class must be splittable — methods depending on source module services can be cleanly separated |\n| **Time estimate** | 30–60 min depending on method count and test coverage |\n| **Risk** | Medium — requires careful analysis to avoid logic duplication |\n\n### Steps\n\n1. **Analyze method dependencies** of the class to be migrated\n   - Mark methods that only depend on target module internals → **keep in base class**\n   - Mark methods that depend on source module services → **extract to business service**\n\n   > **Checkpoint 1** — Before splitting, verify:\n   > - [ ] Every method is categorized (no \"unclassified\" methods remain)\n   > - [ ] Methods marked for extraction **only** call source module services (not a mix of both)\n   > - [ ] If a method calls both, consider extracting a smaller helper method first\n   > - [ ] **STOP — present the split plan to the user and wait for explicit confirmation** before proceeding. Do not autonomously decide which methods stay vs. move.\n\n2. **Migrate the base class** to the target module\n   - Move the file to the target module\n   - Remove extracted methods\n   - Add `implements IXxxService`\n   - Make remaining methods `public` if they were previously `private/protected` and needed by the business service\n\n   > **Checkpoint 2** — After migration, verify:\n   > - [ ] Base class has **zero** imports from the source module\n   > - [ ] `./gradlew :module-b:compileJava` or `mvn -pl module-b compile` passes\n   > - [ ] All extracted methods are **gone** from the base class (grep for their names to confirm)\n\n3. **Create the Business Service** in the source module\n   - Name: `XxxBusinessService`\n   - Inject the migrated base class (via interface) and all required source module dependencies\n     - **Keep the original field name** (e.g., `private final IUserService userService`) — this refers to the original bean, so the name must match the original bean name\n   - Implement extracted methods using injected dependencies\n\n   > **Checkpoint 3** — After creation, verify:\n   > - [ ] Business Service **only** calls base class methods via the interface (never direct concrete class)\n   > - [ ] No logic duplication: extracted methods delegate to base class rather than re-implementing\n   > - [ ] `./gradlew :module-a:compileJava` or `mvn -pl module-a compile` passes\n\n4. **Update all callers**\n   - Callers that used extracted methods → reference `XxxBusinessService`\n     - **Field names MUST be renamed**: change `xxxService` to `xxxBusinessService` to match the new Spring bean name\n     - Example: `private UserService userService` → `private UserBusinessService userBusinessService`\n     - **Why**: Spring generates bean names from the class name (lowercasing the first letter). The original bean is `userService`; the new one is `userBusinessService`. If the field name is not renamed, `@Resource`, `@Autowired` + `@Qualifier`, or framework-specific injection may match the original bean by name and inject the wrong type (the original class instead of the Business Service), causing startup failures or runtime NPEs\n   - Callers that used base methods → reference interface (already handled by Pattern 1)\n   - If a caller needs both base and extracted methods, inject both fields (names must differ):\n     ```java\n     private final IUserService userService;                 // base methods, bean name stays userService\n     private final UserBusinessService userBusinessService;  // extracted methods, bean name is userBusinessService\n     ```\n\n   > **Checkpoint 4** — Final verification:\n   > - [ ] All field names referencing `XxxBusinessService` are renamed (`grep -r \"UserBusinessService userService\" module-a/src/` returns empty)\n   > - [ ] All references to the original concrete class in the source module are gone\n   > - [ ] `./gradlew compileJava compileTestJava` or `mvn compile test-compile` passes on **all** affected modules\n   > - [ ] Run the full test suite (`./gradlew test` or `mvn test`) — behavior must be identical after refactoring\n\n   > **⚠️ If tests fail:** Use `git diff` to compare pre/post refactoring. Most common cause: a method was extracted but still called another method that stayed in the base class. Fix by adding that method to the interface.\n\n### Example\n\n```java\n// BEFORE: UserService has mixed dependencies\n// module-a/.../UserService.java\n@Service\npublic class UserService {\n    private final UserRepository repo;        // internal\n    private final PermissionService auth;     // from module-a!\n\n    public UserProfile getById(Long id) { return repo.selectById(id); }  // stays\n    public List<String> listAdmins(Long id) {   // extract\n        return auth.listAdmins(repo.selectById(id).getAppId());\n    }\n}\n\n// AFTER: Base class migrated, Business Service extracted\n// module-b/.../UserService.java  (pure — zero imports from module-a)\n@Service\npublic class UserService implements IUserService {\n    private final UserRepository repo;\n    public UserProfile getById(Long id) { return repo.selectById(id); }\n}\n\n// module-a/.../UserBusinessService.java  (orchestrates cross-module logic)\n@Service\npublic class UserBusinessService {\n    // NOTE: internal field name STAYS as userService — it refers to the original bean via interface\n    private final IUserService userService;   // migrated base class via interface (bean name = userService)\n    private final PermissionService auth;     // source-module dependency\n    public List<String> listAdmins(Long id) {\n        // delegates to base class method instead of duplicating repo access\n        return auth.listAdmins(userService.getById(id).getAppId());\n    }\n}\n\n// module-a/.../OrderService.java  (caller — field name MUST be renamed!)\n@Service\npublic class OrderService {\n    // ❌ WRONG: field name not renamed; Spring injects the original UserService bean by name, not UserBusinessService\n    // @Autowired private UserBusinessService userService;\n\n    // ✅ CORRECT: field name matches the new bean name userBusinessService\n    private final UserBusinessService userBusinessService;\n\n    public void notifyAdmins(Long userId) {\n        List<String> admins = userBusinessService.listAdmins(userId);  // call extracted method\n    }\n}\n```\n\n### Success Criteria\n\n| Criterion | Verification |\n|-----------|-------------|\n| Base class is pure | `grep \"import com.example.module-a\" module-b/src/.../BaseClass.java` returns empty |\n| Business Service delegates correctly | All extracted methods call base class via interface, never direct concrete class |\n| Field names renamed | `grep -r \"UserBusinessService userService\" module-a/src/` returns empty (field names match the new bean names) |\n| No logic duplication | `diff` extracted methods against originals — should delegate, not duplicate |\n| Full compilation | `./gradlew compileJava compileTestJava` or `mvn compile test-compile` passes on all affected modules |\n| Behavior preserved | `./gradlew test` or `mvn test` passes — no functional changes |\n\n---\n\n## Decision Tree\n\n```\nCan the class be moved to target module without breaking anything?\n│\n├─ YES → Use Pattern 1: Interface Extraction\n│        (create interface, implement, replace references)\n│\n└─ NO (some methods depend on source module services)\n    │\n    └─ Use Pattern 2: Business Service Extraction\n      (split class: base → target module, business → source module)\n```\n\n## Common Pitfalls\n\n| Pitfall | Why It Happens | Fix |\n|---------|---------------|-----|\n| Missed a caller | `grep` only covers `.java` files, but XML/JSON configs may reference bean names | Also search in `*.xml`, `*.yml`, `*.properties` |\n| Interface method signature mismatch | Return type or parameter type changed during copy | Use `javap -public` on concrete class to verify |\n| Spring bean name collision | Two beans with same class name in different modules | Use `@Qualifier` or explicit bean names |\n| `@Mock` in tests still references concrete class | Test files not updated alongside main code | Update test mocks to interface type simultaneously |\n| Business Service duplicates logic | Extracted method had inline logic that should be reused | Delegate to base class method instead of duplicating |\n| Field name not renamed | Spring injects by bean name; field name `userService` matches the original `UserService` bean instead of `UserBusinessService` | Rename `xxxService` to `xxxBusinessService` to match the new bean name |\n\n## Edge Cases & Recovery\n\n### Hidden Dependency Discovery (after interface extraction)\n\nIf `./gradlew :module-a:compileJava` (or `mvn -pl module-a compile`) fails after removing the dependency:\n\n1. **Read the compiler error** — it names the missing class/dependency\n2. **Check if it's a transitive dependency** — the source module may have been using types from the target module indirectly\n3. **Fix strategy:**\n   - If the type is a DTO/enum → move it to the shared module alongside the interface\n   - If the type is another service → extract its interface too (cascading Pattern 1)\n   - If the type is a utility class → copy (not move) it to the shared module\n\n### Framework & Language Compatibility\n\n| Scenario | Symptom | Fix |\n|----------|---------|-----|\n| Spring bean name collision | `NoUniqueBeanDefinitionException` | `@Qualifier(\"beanName\")` or `@Primary` |\n| Missing bean after interface change | `NoSuchBeanDefinitionException` | Check `@ComponentScan` includes new module |\n| Test mock references concrete class | `BeanNotOfRequiredTypeException` | Update `@Mock` to interface type |\n| Lombok `@RequiredArgsConstructor` | No symptom — works automatically | No change needed (generates based on fields) |\n| Generics in method signatures | `javap` shows raw types | Keep generics as-is; verify with `javap -public -v` |\n| Business Service field name not renamed | `BeanNotOfRequiredTypeException` or wrong bean injected (original type injected into Business Service field) | Rename field from `xxxService` to `xxxBusinessService` to match the Spring-generated bean name |\n\n### Rollback Strategy\n\nIf refactoring causes widespread test failures:\n\n```bash\n# 1. Immediately restore the gradle dependency\ngit checkout -- module-a/build.gradle\n\n# 2. Revert interface references back to concrete class\ngit checkout -- module-a/src/\n\n# 3. Keep the interface and implementation in place (no harm)\n# 4. Fix the root cause, then retry the migration\n```\n\n**Never leave the codebase in a non-compiling state between commits.**\n\n## Co-Migration Pattern\n\nWhen class A depends on class B (same package), and both need to move:\n\n1. Move both classes together\n2. Only extract interface for class A (the one referenced by callers)\n3. Class B can remain concrete if only class A references it\n\nExample: `MessageSender` + `QueueConfig` — both migrate, but only `MessageSender` needs an interface since external callers only reference it.\n\n## Command Cheat Sheet\n\nCopy-paste ready commands for the entire workflow:\n\n```bash\n# === Phase 0: Diagnosis ===\n# Find which module depends on which\n# Gradle:\n./gradlew :module-a:dependencies --configuration compileClasspath | grep module-b\n# Maven:\nmvn dependency:tree -pl module-a | grep module-b\n\n# Find all references to a class across the project\ngrep -r \"NotificationService\" --include=\"*.java\" --include=\"*.xml\" module-a/src/\n\n# Extract public method signatures from a concrete class\njavap -public com.example.module.NotificationService\n\n# === Phase 1: Pattern 1 — Interface Extraction ===\n# Verify interface matches concrete class\njavap -public com.example.interface.INotificationService\n\n# Confirm zero imports remain after migration\ngrep -r \"import com.example.module.NotificationService\" module-a/src/ || echo \"CLEAN\"\n\n# === Phase 2: Pattern 2 — Business Service Extraction ===\n# Verify base class has no source-module imports\ngrep \"import com.example.module-a\" module-b/src/.../UserService.java || echo \"CLEAN\"\n\n# Verify all Business Service field names are renamed (must return EMPTY)\n# Checks field declarations, constructor params, and annotated fields\ngrep -rE \"UserBusinessService\\s+userService|@.*UserBusinessService.*userService|UserBusinessService\\(.*userService\" module-a/src/ || echo \"CLEAN\"\n\n# Also verify Business Service internal fields still reference the original bean name\ngrep -rE \"IUserService\\s+userBusinessService\" module-a/src/ || echo \"CLEAN\"\n\n# === Phase 3: Verification ===\n# Compile all affected modules in order\n# Gradle:\n./gradlew :module-interface:compileJava\n./gradlew :module-b:compileJava\n./gradlew :module-a:compileJava\n./gradlew :module-a:compileTestJava\n# Maven:\nmvn -pl module-interface compile\nmvn -pl module-b compile\nmvn -pl module-a compile\nmvn -pl module-a test-compile\n\n# Full test suite\n# Gradle:\n./gradlew :module-a:test :module-b:test\n# Maven:\nmvn -pl module-a,module-b test\n\n# Final dependency check — must return EMPTY\n# Gradle:\n./gradlew :module-a:dependencies --configuration compileClasspath | grep module-b\n# Maven:\nmvn dependency:tree -pl module-a | grep module-b\n```\n\n","topics":["Business"],"tags":{"latest":"1.0.1"},"stats":{"comments":0,"downloads":421,"installsAllTime":16,"installsCurrent":0,"stars":0,"versions":2},"createdAt":1777555997064,"updatedAt":1778492814510},"latestVersion":{"version":"1.0.1","createdAt":1778325145755,"changelog":"Version 1.0.1\n\n- fix layer bean name","license":"MIT-0"},"metadata":null,"owner":{"handle":"naozixu","userId":"s176r04pffg7fhpyc0311y2wk985v7sn","displayName":"Naozi","image":"https://avatars.githubusercontent.com/u/17685614?v=4"},"moderation":null}