Skip to content

Repository files navigation

Microservices Simulator

Table of Contents

Overview

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.

In practice, this allows developers to compare Saga semantic-lock strategies against TCC snapshot-based conflict resolution without rewriting domain services, and benchmark the same workflows across local, stream, and gRPC deployment profiles.

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.

Architecture

Architecture

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.

TopologyProcess and Data LayoutCommand TransportEvent TransportTypical ProfilesStrategic ValueCore Infrastructure
Centralized LocalSingle application process, shared databaseIn-memory (local)Internal event persistence and pollingsagas|tcc, localDeterministic baseline to debug invariants, workflow ordering, and concurrency interleavingsPostgreSQL, Jaeger
Centralized StreamSingle application process, shared databaseRabbitMQ command channelsRabbitMQ event-channelsagas|tcc, streamIntermediate topology to benchmark broker-based communication with shared persistencePostgreSQL, RabbitMQ, Jaeger
Centralized gRPCSingle application process, shared databasegRPC (discovery-based resolution)RabbitMQ event-channelsagas|tcc, grpcIntermediate topology to benchmark point-to-point RPC and discovery with shared persistencePostgreSQL, Eureka, RabbitMQ, Jaeger
Distributed StreamIndependent service processes, database-per-serviceRabbitMQ command channelsRabbitMQ event-channelService profile + sagas|tcc, stream (e.g., quiz-service, sagas, stream)Production-like isolation with broker-mediated coordination across independent servicesPostgreSQL per service, Eureka or Spring Cloud Kubernetes, API Gateway, RabbitMQ, Jaeger
Distributed gRPCIndependent service processes, database-per-servicegRPC (service-to-service via discovery)RabbitMQ event-channelService profile + sagas|tcc, grpc (e.g., quiz-service, tcc, grpc)Production-like hybrid model with direct RPC commands and asynchronous event propagationPostgreSQL 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 because causal conflict resolution depends on a centralized, monotonically increasing version sequence.

Code Structure

Simulator

ModulePurposeMain Code
AggregateAggregate identity, invariants, and event subscription contractssimulator aggregate package
CoordinationWorkflow orchestration, step dependency ordering, and execution planssimulator coordination package
Transaction (Sagas)Semantic locks, compensation, and Saga-specific command wrapperssimulator sagas package
Transaction (TCC)Causal snapshots, optimistic conflict detection, and merge commit pathsimulator causal package
NotificationEvent persistence, publication/subscription transport, and polling supportsimulator notification package
MessagingCommand gateway abstraction across local, stream, and gRPC modessimulator messaging package
ImpairmentFault and delay injection hooks for resilience and behavior testingsimulator impairment package
MonitoringTracing and observability support for functionalities and workflow stepssimulator monitoring package
VersioningVersion ID generation and version-service support across topologiessimulator versioning package

Quizzes Microservice System

  • 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

Application Decomposition

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.

How to Implement Your Own Business Logic

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.

While the majority of the effort is focused on the microservice being developed, supporting centralized deployment topologies (such as Centralized Stream or Centralized gRPC) introduces a minor configuration step: the developer must append the new aggregate's specific network bindings (e.g., stream channels and event broker bindings) into a shared, unified application.yaml file to ensure the centralized execution environment can correctly route messages between the aggregates.

Implementing a Single Aggregate

Development TaskImplementation Details & ExampleRationale (Why)
Define Spring Boot ApplicationCreate the microservice entry point, e.g., TournamentServiceApplication.java with @SpringBootApplication.Establishes the bounded context runtime and independent deployability.
Define AggregateDefine 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 RepositoriesCreate 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 InvariantsOverride the verifyInvariants() method, e.g., asserting tournament start date is before end date.Prevents invalid aggregate versions from being committed.
Define EventsDefine the events published/subscribed, e.g., UpdateStudentNameEvent.java.Makes upstream changes observable by downstream aggregates for eventual consistency.
Subscribe EventsOverride the getEventSubscriptions() method, adding concrete subscriptions.Declares upstream-downstream dependencies explicitly at the domain level.
Define Event SubscriptionsDefine 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 HandlersDelegate handling to processing functionalities, e.g., UpdateStudentNameEventHandler.java.Converts raw event intake into deterministic domain actions.
Define Aggregate ServicesDefine the microservice API to register changes, e.g., updateUserName(...).Provides stable operation-level contracts used by commands and controllers.
Define Web ControllersExpose REST API endpoints to external clients, e.g., TournamentController.java.Enables external access while preserving application/domain layering.
Define Event HandlingDefine polling logic for the event table, e.g., TournamentEventHandling.java.Drives periodic event processing cycles for eventual consistency.
Define Event Subscriber ServiceSubscribe to Spring Cloud Stream events, e.g., EventSubscriberService.java.Bridges broker transport to local event persistence/processing.
Define Transactional AggregatesExtend 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 CommandsDefine remote commands for aggregate services, e.g., AddParticipantCommand.java.Formalizes inter-service invocation contracts independent of transport protocol.
Create CommandHandlerReceive remote commands and map to services, e.g., TournamentCommandHandler.java.Centralizes command routing and isolates transport concerns from domain services.
Configure Network BindingsSet stream channels or grpc ports in the aggregate yaml file, e.g., in application-tournament-service.yaml and append them to the unified application.yaml.Activates deployment topologies (both distributed and centralized) without changing business code.
Configure API Gateway RoutesDefine route mappings in the microservice yaml to route HTTP requests.Decouples external API paths from internal service locations.

Implementing a Single Functionality

Development TaskImplementation Details & ExampleRationale (Why)
Define FunctionalityExtend WorkflowFunctionality to coordinate a specific use-case, e.g., AddParticipantFunctionalitySagas.java.Encapsulates one business use case as a reusable coordination unit.
Workflow OrchestrationMap execution Steps, dependencies, and transaction triggers within buildWorkflow(), e.g., defining getUserStep and addParticipantStep dependencies.Makes ordering, dependency, and rollback/compensation boundaries explicit.
Command DispatchingInstantiate 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.

Running an Application

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 EnvironmentBest ForDocumentation
Docker ComposeLocal testing, switching between centralized/distributed topologies quickly without local dependencies.Run Using Docker
MavenDevelopment, running individual microservices, and load testing with JMeter.Run Using Maven
IntelliJ IDEADebugging 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

Configuration Reference

The application uses Spring Boot profiles and YAML configuration files to manage different deployment modes.

Jaeger Tracing

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:4317 using the OTLP gRPC protocol.
  • Instrumentation: Custom instrumentation is implemented in TraceManager using the OpenTelemetry SDK to trace functionalities and their steps.
  • Cross-service correlation: Trace IDs are propagated through workflow execution, command dispatching, and event processing, allowing end-to-end inspection of one functionality across microservices.

Service Discovery

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 Configuration

Database settings are defined in application.yaml:

ProfileDatabaseDescription
CentralizedmsdbSingle database for all aggregates
DistributedPer-service DBsEach service has its own database (e.g., tournamentdb, userdb)

When running in distributed mode, the Quizzes microservices map to the following individual databases:

  • answer-service -> answerdb
  • course-service -> coursedb
  • execution-service -> executiondb
  • question-service -> questiondb
  • quiz-service -> quizdb
  • topic-service -> topicdb
  • tournament-service -> tournamentdb
  • user-service -> userdb

Service-specific database URLs are configured in profile files like application-tournament-service.yaml.

Spring Cloud Stream Bindings

When running with the stream profile, inter-service communication uses RabbitMQ. Bindings are configured in application.yaml:

Binding TypeExamplePurpose
Command Channelstournament-command-channelSend commands to services
Command ConsumerstournamentServiceCommandChannel-in-0Receive and process commands
Event Channelevent-channelBroadcast events to subscribers
Event SubscriberstournamentEventSubscriber-in-0Receive events for processing
Response ChannelcommandResponseChannel-in-0Receive command responses

Service-specific bindings override only the channels relevant to that service, as shown in application-tournament-service.yaml.

Operational intent of channel types:

  • Command channels are directional, aggregate-targeted paths for explicit downstream-to-upstream requests.
  • Event channel is a shared broadcast path for upstream-to-downstream propagation and eventual consistency.
  • Response channel supports correlated replies for synchronous behavior over asynchronous transports.

gRPC Command Gateway

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.

Distributed Version Service

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). In TCC, centralized ordering is part of causal conflict detection and merge correctness, so decentralized Snowflake generation is intentionally disabled.

ProfileVersion SourceRequires version-service?
(default)Centralized VersionServiceYes
distributed-versionLocal SnowflakeIdGeneratorNo

Service URLs and Ports

Microservices:

Each microservice runs on a dedicated port:

ServicePortProfile File
Gateway8080application-gateway.yaml
Version Service8081application-version-service.yaml
Answer Service8082application-answer-service.yaml
Course Execution8083application-execution-service.yaml
Question Service8084application-question-service.yaml
Quiz Service8085application-quiz-service.yaml
Topic Service8086application-topic-service.yaml
Tournament Service8087application-tournament-service.yaml
User Service8088application-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.

Observability & Infrastructure:

ServiceURL / PortPurpose
Jaeger UIhttp://localhost:16686Distributed tracing; view spans, traces, service dependencies
RabbitMQ Managementhttp://localhost:15672Message broker admin
Eureka Service Discoveryhttp://localhost:8761View registered services and their instances
PostgreSQLlocalhost:5432Database access (centralized mode: msdb; distributed: *db)

Default Credentials:

  • PostgreSQL: postgres / postgres
  • RabbitMQ: guest / guest

API Gateway Configuration

The Gateway application-gateway.yaml configures:

  1. Service discovery: Eureka discovery for local distributed deployments; Kubernetes discovery is enabled via the kubernetes profile.
  2. 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.imports referencing the target microservice application properties, which the DynamicMVCProxyController uses to forward REST calls.
  3. Version service URL: The Admin controller endpoints directly interact with the remote microservices for configuration sync.

Test Cases

How these tests map to simulator capabilities:

  • Functionality orchestration and ordering: validate workflow step dependencies and execution order.
  • Concurrency interleavings: force race scenarios with controlled step progression (for example, executeUntilStep(...) in coordination tests).
  • Fault and recovery behavior: inject failures and verify compensation and abort paths.
  • Model comparison: run analogous Sagas and TCC tests to compare lock-based and merge-based conflict handling.

Sagas-focused test suites:

TCC-focused test suites:

For deterministic concurrency scenarios and paper-aligned walkthroughs, see Reproducing DAIS2023 Paper Tests.

Benchmarking and Performance Tests

For repeated deployment benchmarking of tournament scenarios, use:

./scripts/benchmark-deployments.sh --test concurrentAddparticipant.jmx

Run with a specific transaction mode and a number of repetitions:

./scripts/benchmark-deployments.sh --test concurrentAddparticipant.jmx --tx-mode sagas --repetitions 5
./scripts/benchmark-deployments.sh --test 5a-updateStudentName-addParticipant-processUpdateNameEvent.jmx --tx-mode tcc --repetitions 5

What this script does:

  • Boots each configured topology (centralized-*, distributed-*, and distributed-version variants).
  • Runs the selected .jmx test for each deployment, across the configured repetitions.
  • Writes per-run and aggregate results under jmeter-results in a timestamped benchmark folder.
  • Returns non-zero on benchmark failures, so it can be used in CI/regression workflows.

Requirements and setup details are in Run Using Maven. DAIS paper scenario mapping is in Reproducing DAIS2023 Paper Tests.

Publications

License

MIT License

About

No description, website, or topics provided.

Resources

Stars

5 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages