- Overview
- Architecture
- Running an Application
- Configuration Reference
- Test Cases
- Code Structure
- How to Implement Your Own Business Logic
- Publications
Developing business-logic-rich microservices requires navigating complex trade-offs between data consistency and distributed coordination. Although patterns like Sagas and Transactional Causal Consistency (TCC) provide mechanisms to manage distributed state, validating their behavior before production is challenging.
The Microservices Simulator is a Domain-Driven Design (DDD) microservice simulator that isolates core business logic from communication and transactional infrastructure. By modeling distributed systems around aggregates, the simulator allows developers to evaluate identical application code under varying consistency guarantees and network constraints. It features support for multiple transactional models (Sagas, TCC) and seamless transitions across diverse deployment topologies, ranging from centralized execution to fully distributed environments.
This tool acts as a deterministic sandbox for the shift-left validation and optimization of microservice architectures, minimizing developer effort while enabling robust architectural validation.
The system architecture is divided into three primary layers:
- Application Layer: Contains the concrete domain logic, specifically, the Application Functionality and Application Domain components. This layer is entirely decoupled from the underlying infrastructural complexities.
- Business Layer: Provides the core coordination and domain structuring mechanisms. It encompasses the Coordination Module, the Transaction Module, and the Aggregate Module.
- Infrastructure Layer: Manages cross-cutting technical concerns and network operations, including the Messaging Module, Notification Module, Impairment Module, Monitoring Module, and Versioning Module.
The simulator supports multiple execution topologies, ranging from deterministic single-process runs to fully distributed microservice deployments.
| Topology | Process and Data Layout | Command Transport | Event Transport | Typical Profiles | Core Infrastructure |
|---|---|---|---|---|---|
| Centralized Local | Single application process, shared database | In-memory (local) | Internal event persistence and polling | sagas|tcc, local |
PostgreSQL, Jaeger |
| Centralized Stream | Single application process, shared database | RabbitMQ command channels | RabbitMQ event-channel |
sagas|tcc, stream |
PostgreSQL, RabbitMQ, Jaeger |
| Centralized gRPC | Single application process, shared database | gRPC (discovery-based resolution) | RabbitMQ event-channel |
sagas|tcc, grpc |
PostgreSQL, Eureka, RabbitMQ, Jaeger |
| Distributed Stream | Independent service processes, database-per-service | RabbitMQ command channels | RabbitMQ event-channel |
Service profile + sagas|tcc, stream (e.g., quiz-service, sagas, stream) |
PostgreSQL per service, Eureka or Spring Cloud Kubernetes, API Gateway, RabbitMQ, Jaeger |
| Distributed gRPC | Independent service processes, database-per-service | gRPC (service-to-service via discovery) | RabbitMQ event-channel |
Service profile + sagas|tcc, grpc (e.g., quiz-service, tcc, grpc) |
PostgreSQL per service, Eureka or Spring Cloud Kubernetes, API Gateway, RabbitMQ, Jaeger |
Versioning option across topologies: add distributed-version only with sagas to use local Snowflake ID generation; tcc requires centralized version management.
The simulator framework acts as the foundation for microservice applications. You can implement multiple applications in
the applications/ directory. The Quizzes application is provided as a complete reference implementation and case
study.
Running the simulator effectively means running an application (like Quizzes) built on top of it. The execution framework provides extensive flexibility depending on your goals, whether debugging domain logic locally or testing distributed resilience on Kubernetes.
| Execution Environment | Best For | Documentation |
|---|---|---|
| Docker Compose | Local testing, switching between centralized/distributed topologies quickly without local dependencies. | Run Using Docker |
| Maven | Development, running individual microservices, and load testing with JMeter. | Run Using Maven |
| IntelliJ IDEA | Debugging and stepping through execution flows using pre-configured run profiles. | Run Using IntelliJ |
| Kubernetes (Local/Cloud) | Testing production-grade orchestration (Kind) or cloud latency (Azure AKS). | Deploy to Kubernetes |
The application uses Spring Boot profiles and YAML configuration files to manage different deployment modes.
The project uses Jaeger for distributed tracing to monitor and visualize the flow of requests across microservices.
- Dashboard: Access the Jaeger UI at http://localhost:16686.
- Collector: The application sends traces to the Jaeger collector on
http://localhost:4317using the OTLP gRPC protocol. - Instrumentation: Custom instrumentation is implemented in
TraceManagerusing the OpenTelemetry SDK to trace functionalities and their steps.
In distributed mode, local deployments use Eureka for service discovery. The gateway and each microservice register with
the Eureka server at http://${EUREKA_HOST:localhost}:8761/eureka/. When deploying on Kubernetes, the kubernetes
profile enables Spring Cloud Kubernetes discovery instead of Eureka.
Database settings are defined in application.yaml:
| Profile | Database | Description |
|---|---|---|
| Centralized | msdb |
Single database for all aggregates |
| Distributed | Per-service DBs | Each service has its own database (e.g., tournamentdb, userdb) |
Service-specific database URLs are configured in profile files like application-tournament-service.yaml.
When running with the stream profile, inter-service communication uses RabbitMQ. Bindings are configured
in application.yaml:
| Binding Type | Example | Purpose |
|---|---|---|
| Command Channels | tournament-command-channel |
Send commands to services |
| Command Consumers | tournamentServiceCommandChannel-in-0 |
Receive and process commands |
| Event Channel | event-channel |
Broadcast events to subscribers |
| Event Subscribers | tournamentEventSubscriber-in-0 |
Receive events for processing |
| Response Channel | commandResponseChannel-in-0 |
Receive command responses |
Service-specific bindings override only the channels relevant to that service, as shown in application-tournament-service.yaml.
Alternative remote transport is available with the grpc profile. Each service exposes a gRPC endpoint for
commands (see GrpcServerRunner), and callers use GrpcCommandGateway with Eureka-based discovery. Default and
service-specific gRPC ports are configured in the application-*-service.yaml files (and exposed via Eureka metadata
key grpcPort). Override the default client port with grpc.command.default-port or per-service with
grpc.command.<service>.port when needed.
When running in distributed mode with the distributed-version profile active, each microservice generates version IDs
locally using
a Snowflake ID
generator, removing the need for a centralized version-service. This profile can also be used in centralized mode with
any communication profile (local, stream, or grpc). The 64-bit IDs are composed of a 41-bit timestamp, a 10-bit
machine ID (derived from spring.application.name), and a 12-bit sequence number, guaranteeing globally unique,
monotonically increasing versions across services.
This option is only supported with the sagas transactional model (TCC requires centralized version management).
| Profile | Version Source | Requires version-service? |
|---|---|---|
| (default) | Centralized VersionService |
Yes |
distributed-version |
Local SnowflakeIdGenerator |
No |
Each microservice runs on a dedicated port:
| Service | Port | Profile File |
|---|---|---|
| Gateway | 8080 | application-gateway.yaml |
| Version Service | 8081 | application-version-service.yaml |
| Answer Service | 8082 | application-answer-service.yaml |
| Course Execution | 8083 | application-execution-service.yaml |
| Question Service | 8084 | application-question-service.yaml |
| Quiz Service | 8085 | application-quiz-service.yaml |
| Topic Service | 8086 | application-topic-service.yaml |
| Tournament Service | 8087 | application-tournament-service.yaml |
| User Service | 8088 | application-user-service.yaml |
Every service port can be changed, including version-service port 8081, and gateway port 8080. Service Discovery
will map the service name to the service port automatically.
The Gateway application-gateway.yaml configures:
- Service discovery: Eureka discovery for local distributed deployments; Kubernetes discovery is enabled via the
kubernetesprofile. - Route definitions: The API Gateway is a Spring MVC-based application that dynamically proxies HTTP requests to
backend services. Routes are configured via
gateway.routes.importsreferencing the target microservice application properties, which theDynamicMVCProxyControlleruses to forward REST calls. - Version service URL: The Admin controller endpoints directly interact with the remote microservices for configuration sync.
Sagas test cases:
- Workflow Test Plan (Simulator)
- Circuit Breaker Tests (Simulator)
- Tournament Functionality Tests (Quizzes)
- Tournament Async Coordination Tests (Quizzes)
TCC test cases:
For details on testing complex concurrency interleavings from the DAIS2023 paper, see Reproducing DAIS2023 Paper Tests.
- The core concepts of Domain-Driven Design
- The core concepts for the distributed functionalities Coordination
- The core concepts for management of Sagas
- The core concepts for management of TCC
- A case study for Quizzes Tutor
- The transactional model independent Microservices
- The Sagas implementation for Aggregates and Coordination
- The TCC implementation for Aggregates and Coordination
- The tests of the Quizzes Tutor for Sagas and TCC
The API Gateway is used when running the quizzes application as microservices to route API requests to the appropriate microservice. The gateway operates as an MVC application using a custom dynamic proxy controller to forward REST requests.
The framework significantly minimizes the cognitive load for developers by abstracting distributed infrastructure. The workflow focuses strictly on domain modeling, defining events, and orchestrating business logic.
| Development Task | Implementation Details & Example | Rationale (Why) |
|---|---|---|
| Define Spring Boot Application | Create the microservice entry point, e.g., TournamentServiceApplication.java with @SpringBootApplication. |
Establishes the bounded context runtime and independent deployability. |
| Define Aggregate | Define the JPA root entity, e.g., Tournament.java, and associated value objects, e.g., TournamentCreator. |
Defines the transactional consistency boundary where invariants are enforced. |
| Define DTOs and Repositories | Create data transfer objects and Spring Data JPA interfaces for data access, e.g., TournamentDto.java, TournamentRepository.java. |
Separates persistence/API contracts from domain behavior and supports query/update paths. |
| Specify Invariants | Override the verifyInvariants() method, e.g., asserting tournament start date is before end date. |
Prevents invalid aggregate versions from being committed. |
| Define Events | Define the events published/subscribed, e.g., UpdateStudentNameEvent.java. |
Makes upstream changes observable by downstream aggregates for eventual consistency. |
| Subscribe Events | Override the getEventSubscriptions() method, adding concrete subscriptions. |
Declares upstream-downstream dependencies explicitly at the domain level. |
| Define Event Subscriptions | Define subscription conditions, e.g., in TournamentSubscribesUpdateStudentName.java a tournament subscribes to creator/participant name updates. |
Filters only relevant upstream events, avoiding unnecessary or inconsistent updates. |
| Define Event Handlers | Delegate handling to processing functionalities, e.g., UpdateStudentNameEventHandler.java. |
Converts raw event intake into deterministic domain actions. |
| Define Aggregate Services | Define the microservice API to register changes, e.g., updateUserName(...). |
Provides stable operation-level contracts used by commands and controllers. |
| Define Web Controllers | Expose REST API endpoints to external clients, e.g., TournamentController.java. |
Enables external access while preserving application/domain layering. |
| Define Event Handling | Define polling logic for the event table, e.g., TournamentEventHandling.java. |
Drives periodic event processing cycles for eventual consistency. |
| Define Event Subscriber Service | Subscribe to Spring Cloud Stream events, e.g., EventSubscriberService.java. |
Bridges broker transport to local event persistence/processing. |
| Define Transactional Aggregates | Extend aggregate for specific models, e.g., SagaTournament.java (locks) and CausalTournament.java (merging). |
Adapts the same domain to model-specific consistency semantics without duplicating business logic. |
| Define Commands | Define remote commands for aggregate services, e.g., AddParticipantCommand.java. |
Formalizes inter-service invocation contracts independent of transport protocol. |
| Create CommandHandler | Receive remote commands and map to services, e.g., TournamentCommandHandler.java. |
Centralizes command routing and isolates transport concerns from domain services. |
| Configure Network Bindings | Set stream channels or grpc ports in application-tournament-service.yaml. |
Activates a deployment topology without changing business code. |
| Configure API Gateway Routes | Define route mappings in the microservice yaml to route HTTP requests. | Decouples external API paths from internal service locations. |
| Development Task | Implementation Details & Example | Rationale (Why) |
|---|---|---|
| Define Functionality | Extend WorkflowFunctionality to coordinate a specific use-case, e.g., AddParticipantFunctionalitySagas.java. |
Encapsulates one business use case as a reusable coordination unit. |
| Workflow Orchestration | Map execution Steps, dependencies, and transaction triggers within buildWorkflow(), e.g., defining getUserStep and addParticipantStep dependencies. |
Makes ordering, dependency, and rollback/compensation boundaries explicit. |
| Command Dispatching | Instantiate remote Commands and dispatch via the abstract CommandGateway, e.g., sending AddParticipantCommand wrapped in a SagaCommand with semantic locks. |
Executes distributed steps through transport-agnostic contracts while preserving domain isolation. |
For step-by-step execution details of your own business logic or new aggregates:
- Identify upstream dependencies and subscribe to the correct events in your Aggregate class.
- Implement transaction-specific structures (like
SagaStateormergeFields). - Define the workflows that coordinate changes utilizing
CommandGatewayto send requests across microservices. - Update
application.yamlbindings so streams or gRPC channels resolve correctly to the new microservice.
- DAIS 2023: D. Pereira and A. R. Silva, "Transactional Causal Consistent Microservices Simulator," in Distributed Applications and Interoperable Systems (DAIS), 2023.
