This content originally appeared on DEV Community and was authored by dumbestprogrammer
So I tried to merge two Spring-Boot microservices.
The build exploded with cyclic-dependency errors.
And here’s why and how I fixed it.
This post here documents the architectural flow, technical choices,
what I attempted, and where I hit a wall.
And eventually, my solution for it - not by brute force, but by rethinking everything.
Architecture Overview & Current Working Flow
Tech Stack - Java, SpringBoot, REST, Feign, Microservices architecture.
Service A - port 8082:
This service handles the file inputs and parsing structured information from them, followed by external data enrichment.
Input Processing (Service-A)
POST http://localhost:8082/api/upload
Accepts a structured file input(pdf/text) and returns a session-specific UUID to track the processing pipeline.
Primary Analysis (Service-A)
GET http://localhost:8082/api/report/{uploadId}?force=true
Triggers the data extraction and enrichment process, returning a fully processed result (a report).
Service B - port 8083:
Now this service consumes the enriched output from Service A, runs deeper analyses, and produces a refined result that’s used in the platform’s final output.
Secondary Analysis (Service-B)
GET http://localhost:8083/api/trust/report/{uploadId}
This service uses a feign client to communicate with service-A, pulls the enriched output from it, analyzes it, and extracts what it needs for its own computations, does the computations,
and returns a different, more refined, insight-driven result (a report of its own which is like an extension to the service-A's report).
- Each service communicates over HTTP using Feign clients and follows a strict separation of concerns to maintain modularity and scalability. At this point, Service B depended on Service A, but not the other way around ... ensuring a unidirectional and loosely-coupled architecture.
What I Wanted to Do?
I wanted to enhance the user experience and simplify the frontend integration.
Wanted to merge two separate backend responses into one.
Instead of having the client call two different endpoints, the idea was:
“Why not have Service A internally call Service B, fetch the secondary analysis result, and return both in a single consolidated response?”
And being honest, thinking about it ..at first glance this felt like a smart, clean solution.
I mean the flow was extremely straightforward:
- User requests a report from Service-A
- Service-A does its thing and calls Service-B to get its report.
- Service-A combines both reports
- User receives unified response
But in practice? It broke everything.
As to fetch trust data from Service-B, I created a ReportClient Feign interface in Service-A that would talk to Service-B.
Attempted to inject that client inside the report generation logic in Service-A.
- Adding Feign Client in Service-A:
@FeignClient(name = "service-b-client", url = "${feign.clients.service-b.url}")
public interface FeignServiceBClient {
@GetMapping("/api/trust/report/{uploadId}")
TrustReportDTO getTrustReport(@PathVariable("uploadId") String uploadId);
}
- And then enhancing the Report Builder:
@Transactional
public CombinedReportDTO generate(Session session, boolean force, boolean includeB) {
CombinedReportDTO report = // existing logic for Service A’s analysis
if (includeB) {
TrustReportDTO trustReport = feignServiceBClient.getTrustReport(session.getId());
report.setSecondaryAnalysis(trustReport);
}
return report;
}
The approach was methodical and followed established patterns for microservice composition, but now I was facing cyclic-dependency errors.
Cyclic Dependency Reality Check &
Technical Deep Dive
Upon implementation, I encountered cyclic dependency warnings in the IDE and module dependency errors during compilation.
java: Annotation processing is not supported for module cycles.
Please ensure that all modules from cycle [service-b, service-a] are excluded from annotation processing
The root cause became clear:
The Existing Architecture Already Had a Cycle.
In simple terms:
Service A was now calling Service B
But elsewhere, Service-B was already depending on Service-A (e.g., to fetch analysis output
)
Result: cyclic dependency between modules
...
Why Cycles Break??
This wasn’t just a conceptual issue; it was a real, deeply technical roadblock. Understanding why cycles break in Spring Boot and Java helped clarify both architectural and implementation-level decisions.
1. Bean Wiring Cycles (Spring ApplicationContext)
Spring’s ApplicationContext
attempts to resolve all @Autowired
dependencies during startup.
When Service-A
has a Feign client
for Service-B
, and Service-B
also has a Feign client
for Service-A
, Spring is unable to determine a safe initialization order.
This results in a circular dependency exception, halting application startup, even though the services might be logically valid at runtime.
2. Module-Level Dependency Violations (Java Build System)
Even if Spring could theoretically resolve things at runtime, Java itself doesn't allow compile-time cycles across modules (Build-time cycle
).
When both services depend on each other's DTOs, interfaces, or configs, the build system can't resolve the graph.
This breaks the Java Module System and flags cyclic imports, preventing compilation altogether.
3. Annotation Processing Conflicts (Feign, Lombok, etc.)
Tools like Feign, Lombok, and Spring Boot’s auto-configuration all rely on annotation processing at compile-time.
Cyclic references
confuse these processors or cause them to behave unpredictably.
Sometimes even resulting in infinite loops or partially generated code.
Since Feign itself relies on Spring’s annotation processor, this made the situation worse when both services had Feign clients pointing to each other.
My Realization
1. Microservice Boundaries
The fact that users want unified data doesn't mean services should be tightly coupled.
The separation of concerns that led to two services was correct, as both the services, Service-A and Service-B, are actually very different domains with different computational requirements.
- (i) While to me, it initially seemed very user-friendly to have Service-A call Service-B and then return a single merged report.. I realized this would introduce tight coupling and violate the core principles of microservice architecture.
Because Service-B is intentionally designed to consume the output from Service-A, process it independently and return its own specialized analysis.. its own unique report.
And if I reverse this by making Service-A depend on Service-B ..
It would not only create a compile-time cyclic dependency (due to mutual Feign clients and module cycles)
but it will also destroy the clean one-way flow between services.
More critically, it would reduce fault tolerance....so if Service-B goes down then Service-A is going to break too, and as a result the user would see no output at all.
With the current design, both services can continue to operate and return partial results independently.
It made me understand the boundaries between microservices and what
"one-way" actually means in request flow.
This setup of mine violated a key architectural principle:
Microservices should follow a one-directional flow
I knew it, but all this made it extremely clear to me that - One microservice should not call the other and also expect to be called back by it, and also that here the - Flow is One-Way, Not Two-Way :
[User/Frontend] → Service-A → Service-B → returns Info → Service-A returns Final Report
I don’t need Service B to send anything back "later" to Service A. It’s a request-response not an event stream or push system.
(ii) I also considered merging both services into one, but again, that would completely defeat the purpose of having a microservices architecture in the first place.....
where each service is responsible for a single, focused concern and can evolve or scale independently.(iii) And lastly, if I tried to "decouple" Service-B from Service-A just so Service-A could take over and call B instead..
It would force me to duplicate all of Service-A's logic inside Service-B, just to recreate the same output that I was getting before.
which would be not only redundant and error-prone but obviously a clear example of a really bad design.
In short, trying to return a merged report directly from Service-A breaks modularity, reduces resilience, increases maintenance cost, and discards the very architecture I’m trying to preserve.
2. Synchronous Integration Complexity
Attempting synchronous service-to-service communication introduces failure coupling and dependency management complexity that often outweighs the benefits almost all the time.
3. Feign
At one point, I caught myself thinking,
"Maybe Feign can receive requests too? I mean, it’s everywhere else...
"
Spoiler: It can’t.
Feign sends HTTP requests; it doesn’t receive them.
That’s the controller’s job.
Feign is a declarative REST client.
In simple words, Feign is for calling other services, not receiving from them.
4. Aggregation Layer Responsibility
I think in a proper microservice setup, the merging of data shouldn't happen inside the services.
That kind of aggregation feels more like something the frontend or an API gateway should handle.
I feel backend services should remain focused on their core functionalities.
Final Solution
So instead of breaking or violating my clean architecture ..
I asked myself :
“What is the real user need here?
”
The answer was simple :
And it's obvious the user just wants to see both reports for the same upload ID, preferably in the same UI session
.
So I decided to keep both services fully independent and decoupled.
Let the frontend make two parallel API calls and stitch the results together.
One to get the report-A from Service-A.
Another to get the report-B from Service-B.
Two Independent Endpoints:
GET/api/service-a/report/{uploadId} # Output from Service-A
GET/api/service-b/report/{uploadId} # Output from Service-B
Frontend Implementation:
const [citationData, trustData] = await Promise.allSettled([
fetch(`/api/report/${uploadId}`),
fetch(`/api/trust/report/${uploadId}`)
]);
// Display unified UI with both datasets
renderUnifiedReport(citationData.value, trustData.value);
UI Design Approaches
To present both reports cleanly while keeping the services decoupled,
I explored a few frontend strategies:
Side-by-side panels:
Display both the Service-A and Service-B reports together, letting users compare at a glance.Tabbed interface:
Let users toggle between the two reports, each pulled independently.Progressive loading:
Show the Service-A's report instantly, while the Service-B's report loads in the background as it's fetched.
Both reports are keyed by the same uploadId, so there's no need for Service A to embed Service B’s output.
The frontend can query each service directly and combine them on the UI side.
It kept the services decoupled, preserved the architecture I had, and honestly made things easier to debug and scale later.
Benefits of the Final Architecture
-
Zero cyclic dependencies
- services remain completely independent.
-
Independent scalability
- each service can scale based on its computational needs.
-
Fault tolerance
- failure in one service doesn't break the other.
-
Parallel loading
- both reports can be fetched simultaneously for better performance
-
Cleaner codebases
- no complex aggregation logic.
Lessons Learned
Just because users want one response doesn’t mean the backend has to force it. User needs ≠ service design.
Keeping microservices independent isn’t just theory — clean separation wins in the long run: better scaling, better isolation.
Turns out, the frontend’s more powerful than we give it credit for. Parallel fetches, merging responses — totally doable.
Technical Constraints are not bad I guess, haha -
That annoying cycle error? It actually pushed me toward a much cleaner architecture.
Conclusion: Let It Be Separate
What started as a small integration task turned into a much bigger architectural decision.
The cyclic dependency
wasn’t just an error ..it was a reality check.
It made me realize these services were never meant to be stitched together so tightly in the first place.
Sure, maybe I could’ve slapped an @Lazy
annotation somewhere and made the error go away. But who knows what kind of mess that would’ve caused later?
So I stopped forcing it. I let each service do what it does best, and moved the merging logic to the frontend.
Cleaner, simpler, and way more in line with what microservices are supposed to be.
Looking back, I’m glad it broke.
If it hadn’t, I probably would’ve tightly coupled things that were better off independent.
Lesson: If your services push back when you try to force them together… maybe listen.
They’re probably already doing their jobs just fine — on their own.
If you've hit similar issues with service dependencies or Feign loops,
I'd love to hear how you handled it, drop a comment or message me.
This content originally appeared on DEV Community and was authored by dumbestprogrammer

dumbestprogrammer | Sciencx (2025-08-03T17:05:36+00:00) Cyclic Dependency in a Microservice Architecture. Retrieved from https://www.scien.cx/2025/08/03/cyclic-dependency-in-a-microservice-architecture/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.