How should ViewModels and Services communicate in a JavaFX + Spring Boot MVVM application?
Context: I’m building a JavaFX + Spring Boot application using the MVVM pattern: Views are backed by Controllers. Controllers delegate logic to ViewModels. Services handle business logic. I want to maintain separation of concerns and keep communication between ViewModels and Services modular and testable. Scenario: I have a SharedLoginViewModel that manages the workflow state (for example: switching between different login forms or token creation steps). At the same time, a TokenCreationViewModel handles token creation logic (e.g., form validation, submitting token data). Sometimes, TokenCreationViewModel needs to tell SharedLoginViewModel to advance the workflow (e.g., after a token is successfully created). However, this introduces circular dependencies: TokenCreationViewModel → SharedLoginViewModel → LoginWorkflowService But LoginWorkflowService already interacts with TokenCreationViewModel. Attempted Approaches: 1. Direct Reference Pros: Simple, easy to trace. Cons: Causes tight coupling and circular dependencies when multiple ViewModels need to communicate. 2. Spring Events Pros: Decouples components, allows flexible communication. Cons: Adds asynchronous complexity, especially risky for UI workflows where JavaFX thread safety and determinism matter. One reason I’ve been hesitant to use Spring Events for UI flow is that it breaks the straightforward, downward communication chain. In MVVM, I prefer a clear path (Controller → ViewModel → Service) that’s easy to trace and debug. Spring Events work well for backend concerns (e.g., auditing) but feel like overkill for UI-bound workflows. 3. Custom Interface (Current Solution) I introduced a LoginNavigationService interface to decouple navigation logic from the shared ViewModel. Current Solution Step-by-step decoupling using an interface Define the interface: public interface LoginNavigationService { void advanceStep(); void cancel(); } Implement the interface in SharedLoginViewModel @Component public class SharedLoginViewModel implements LoginNavigationService { // Existing workflow logic... @Override public void advanceStep() { advanceLoginStep(); // Internal method to move workflow forward } @Override public void cancel() { cancelWorkflow(); // Internal method to reset workflow } } Inject the interface (not the full SharedLoginViewModel) in TokenCreationViewModel @Component public class TokenCreationViewModel { private final LoginNavigationService navigationService; @Autowired public TokenCreationViewModel(LoginNavigationService navigationService) { this.navigationService = navigationService; } public void onTokenCreated() { // After token creation logic... navigationService.advanceStep(); // Decoupled navigation call } } Alternative Consideration: Listener vs Interface I also considered using listeners between ViewModels, for example: tokenCreationViewModel.setOnTokenCreated(() -> sharedLoginViewModel.advanceStep()); Why I avoided this Creates tight coupling between ViewModels. Requires explicit listener setup at the wiring phase. Why the interface felt cleaner The LoginNavigationService exposes only the navigation-related API. Keeps TokenCreationViewModel unaware of SharedLoginViewModel’s internal logic. Question Is this interface-based approach a good practice? Are there better patterns for ViewModel → Service communication in JavaFX + Spring Boot MVVM?

Context:
I’m building a JavaFX + Spring Boot application using the MVVM pattern:
Views are backed by Controllers.
Controllers delegate logic to ViewModels.
Services handle business logic.
I want to maintain separation of concerns and keep communication between ViewModels and Services modular and testable.
Scenario:
I have a SharedLoginViewModel that manages the workflow state (for example: switching between different login forms or token creation steps).
At the same time, a TokenCreationViewModel handles token creation logic (e.g., form validation, submitting token data).
Sometimes, TokenCreationViewModel needs to tell SharedLoginViewModel to advance the workflow (e.g., after a token is successfully created). However, this introduces circular dependencies:
TokenCreationViewModel → SharedLoginViewModel → LoginWorkflowService
But LoginWorkflowService already interacts with TokenCreationViewModel. Attempted Approaches:
1. Direct Reference
Pros: Simple, easy to trace.
Cons: Causes tight coupling and circular dependencies when multiple ViewModels need to communicate.
2. Spring Events
Pros: Decouples components, allows flexible communication.
Cons: Adds asynchronous complexity, especially risky for UI workflows where JavaFX thread safety and determinism matter.
One reason I’ve been hesitant to use Spring Events for UI flow is that it breaks the straightforward, downward communication chain. In MVVM, I prefer a clear path (Controller → ViewModel → Service) that’s easy to trace and debug. Spring Events work well for backend concerns (e.g., auditing) but feel like overkill for UI-bound workflows.
3. Custom Interface (Current Solution)
I introduced a LoginNavigationService interface to decouple navigation logic from the shared ViewModel.
Current Solution
Step-by-step decoupling using an interface
Define the interface:
public interface LoginNavigationService { void advanceStep(); void cancel(); }
Implement the interface in SharedLoginViewModel
@Component public class SharedLoginViewModel implements LoginNavigationService { // Existing workflow logic... @Override public void advanceStep() { advanceLoginStep(); // Internal method to move workflow forward } @Override public void cancel() { cancelWorkflow(); // Internal method to reset workflow } }
Inject the interface (not the full SharedLoginViewModel) in TokenCreationViewModel
@Component public class TokenCreationViewModel { private final LoginNavigationService navigationService; @Autowired public TokenCreationViewModel(LoginNavigationService navigationService) { this.navigationService = navigationService; } public void onTokenCreated() { // After token creation logic... navigationService.advanceStep(); // Decoupled navigation call } }
Alternative Consideration: Listener vs Interface
I also considered using listeners between ViewModels, for example:
tokenCreationViewModel.setOnTokenCreated(() -> sharedLoginViewModel.advanceStep());
Why I avoided this
Creates tight coupling between ViewModels.
Requires explicit listener setup at the wiring phase.
Why the interface felt cleaner
The LoginNavigationService exposes only the navigation-related API.
Keeps TokenCreationViewModel unaware of SharedLoginViewModel’s internal logic.
Question
Is this interface-based approach a good practice?
Are there better patterns for ViewModel → Service communication in JavaFX + Spring Boot MVVM?