Skip to content

Latest commit

 

History

History
284 lines (214 loc) · 29.9 KB

File metadata and controls

284 lines (214 loc) · 29.9 KB

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.

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.

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.

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 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

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.

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:

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.

Spring Cloud Stream Bindings

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.

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).

Profile Version Source Requires version-service?
(default) Centralized VersionService Yes
distributed-version Local SnowflakeIdGenerator No

Service URLs and Ports

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.

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

Sagas test cases:

TCC test cases:

For details on testing complex concurrency interleavings from the DAIS2023 paper, see Reproducing DAIS2023 Paper Tests.

Code Structure

Simulator

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.

Implementing a Single Aggregate

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.

Implementing a Single Functionality

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:

  1. Identify upstream dependencies and subscribe to the correct events in your Aggregate class.
  2. Implement transaction-specific structures (like SagaState or mergeFields).
  3. Define the workflows that coordinate changes utilizing CommandGateway to send requests across microservices.
  4. Update application.yaml bindings so streams or gRPC channels resolve correctly to the new microservice.

Publications

License

MIT License