This repository is an architecture comparison case study for the same order workflow implemented in two styles:
- a microservices-style implementation with explicit HTTP service boundaries
- a virtual actor-style implementation with identity-based state ownership and serialized execution per actor identity
The project is designed to make architectural trade-offs visible across implementation, state ownership, concurrency, idempotency, compensation, timeout handling, testing, deployment, scaling, observability, operations, organizational fit, and long-term maintenance.
This project is not a benchmark. Local elapsed times are useful for understanding the sample topology, but they should not be interpreted as universal performance conclusions.
The comparison focuses on practical architecture questions:
- Who owns inventory state?
- Who protects inventory invariants?
- Who owns order workflow decisions?
- How is idempotency protected under concurrent duplicate submissions?
- How does each style handle contention for one hot product?
- How are payment failure and timeout compensation expressed?
- How are scenario outcomes tested and documented?
- How do the approaches differ in deployment, scaling, observability, operations, maintenance, and organizational fit?
The microservices-style path uses separate services for the main workflow responsibilities:
Orders.Apiowns order workflow orchestration.Inventory.Apiowns inventory state and reservation invariants.Payments.Apiowns payment authorization behavior.Comparison.Gatewayruns the same scenario against each architecture path.Comparison.Uiprovides the interactive scenario dashboard.
This style makes service boundaries and independent deployment explicit. The trade-off is that every boundary introduces compatibility, reliability, observability, and operational concerns.
The virtual actor-style path expresses the workflow through stateful identities:
OrderGrain(orderId)owns one logical order workflow.InventoryItemGrain(productId)owns one product inventory identity.PaymentAccountGrain(customerId)owns payment behavior.Ordering.Apiexposes the actor-backed workflow entry point.
This style makes identity-based state ownership explicit. The trade-off is that grain interface compatibility, persistent grain state evolution, runtime behavior, activation lifecycle, and hot identity behavior become important design and operational concerns.
The UI exposes the following comparison scenarios. See docs/12-scenario-guide.md for the full scenario guide, expected results, architecture interpretations, and operational notes.
Demonstrates the happy path. Inventory is available, payment succeeds, and the order completes.
Expected shape:
- total request submissions:
1 - unique successful orders:
1 - rejected submissions:
0 - idempotent duplicate responses:
0 - remaining inventory decreases by the requested quantity
Demonstrates business rejection before payment. Inventory is not available, so the order is rejected and payment should not be attempted.
Expected shape:
- total request submissions:
1 - unique successful orders:
0 - rejected submissions:
1 - reason:
InsufficientInventory
Demonstrates compensation after a known downstream failure. Inventory is reserved, payment explicitly fails, and inventory is released.
Expected shape:
- total request submissions:
1 - unique successful orders:
0 - rejected submissions:
1 - remaining inventory returns to initial stock
- reason:
PaymentFailed
Demonstrates timeout handling after inventory has already been reserved. The sample treats timeout as failed, releases inventory, and rejects the order.
Expected shape:
- total request submissions:
1 - unique successful orders:
0 - rejected submissions:
1 - remaining inventory returns to initial stock
- reason:
PaymentTimeout
Demonstrates many independent order submissions competing for the same product stock.
Expected shape when demand exceeds stock:
- unique successful orders do not exceed available stock divided by quantity
- extra submissions are rejected
- remaining inventory does not go below zero
Demonstrates many concurrent requests targeting one hot product identity.
Expected shape with initial stock 25, quantity 1, and 50 concurrent requests:
- total request submissions:
50 - unique successful orders:
25 - rejected submissions:
25 - remaining inventory:
0 - reason:
SomeOrdersRejected
Demonstrates idempotency under repeated duplicate submissions. The scenario submits the same logical order multiple times concurrently using the same order identity and idempotency key.
Expected shape with initial stock 10, quantity 2, and 20 duplicate submissions:
- total request submissions:
20 - unique successful orders:
1 - rejected submissions:
0 - idempotent duplicate responses:
19 - remaining inventory:
8 - reason:
IdempotentResultReturned
The UI result cards use request-submission terminology consistently:
- Total request submissions: how many requests were submitted for the scenario run.
- Unique successful orders: how many unique logical orders completed successfully.
- Rejected submissions: how many logical submissions were rejected.
- Idempotent duplicate responses: how many duplicate submissions returned an existing logical result.
- Remaining inventory: the final inventory quantity after the scenario run.
- Elapsed: local elapsed time for the architecture path in this sample topology.
These values are semantic contracts for the comparison. For example, unique successful orders means unique successful logical orders, not raw successful HTTP responses.
Start the backend services and UI according to the project launch settings. At minimum, the comparison UI expects the comparison gateway and backend services to be running on their configured local ports.
dotnet restore
dotnet build --configuration Release
dotnet test --configuration Release --no-buildThe repository can be run directly from Visual Studio by configuring multiple startup projects.
Set these projects to start together:
src/Microservices/Inventory.Apisrc/Microservices/Payments.Apisrc/Microservices/Orders.Apisrc/VirtualActors/Ordering.Apisrc/Comparison/Comparison.Gatewaysrc/Comparison/Comparison.Ui
Then start debugging/running from Visual Studio and open the UI at:
http://localhost:5000
This is the most convenient local development flow when working inside Visual Studio because each service keeps its own launch profile and output window.
./scripts/run-all-local.ps1This starts the microservices backend, virtual actor backend, comparison gateway, and UI.
./scripts/run-microservices.ps1
./scripts/run-virtual-actors.ps1
./scripts/run-comparison.ps1Open the UI at:
http://localhost:5000
The project includes scenario regression tests that protect the agreed scenario result semantics.
The regression tests cover:
- successful order
- insufficient inventory
- payment failure compensation
- payment timeout after reservation
- concurrent orders
- hot product contention
- duplicate request with concurrent duplicate submissions
The regression suite is intentionally focused on result semantics:
- total request submissions
- unique successful orders
- rejected submissions
- idempotent duplicate responses
- remaining inventory
- reason
When scenario behavior changes intentionally, update the regression tests and the documentation together.
The sample uses lightweight header-based correlation for local diagnostics.
The UI generates a correlation ID and sends it as:
X-Correlation-ID
The gateway forwards the correlation ID to backend calls, and backend services add it to structured logging scopes.
Use the correlation ID shown in the UI to find related logs across:
Comparison.GatewayOrders.ApiInventory.ApiPayments.ApiOrdering.Api
The project deliberately keeps correlation metadata out of scenario request and response contracts. See docs/13-correlation-id-logging.md and docs/16-observability-and-operations.md for more detail.
Read the documentation in this order for the intended narrative:
- Problem explains the comparison problem and the workflow being modeled.
- Microservices design explains the service-based implementation and ownership boundaries.
- Virtual actors design explains the actor-based implementation and stateful identity boundaries.
- Development comparison compares day-to-day development concerns in both styles.
- Deployment comparison compares deployment shape and operational surface area.
- Scaling comparison compares service-boundary scaling with actor-runtime and identity-based scaling.
- Trade-offs summarizes the main architecture trade-offs.
- Organizational scaling and architecture fit explains team ownership, architecture fit, evolution paths, and product-quality risks.
- Local validation describes local validation expectations.
- UI dashboard explains the interactive comparison dashboard.
- End-to-end validation explains end-to-end validation behavior.
- Scenario guide explains each scenario, expected results, architecture interpretations, and operational notes.
- Correlation ID logging explains the simplified correlation mechanism and the production OpenTelemetry direction.
- Release, versioning, and rollback explains architecture-specific compatibility, state evolution, and rollback concerns.
- Maintenance and evolution compares how both styles evolve as behavior changes.
- Observability and operations explains runtime diagnostics, metrics, alerting considerations, and operational interpretation.
- Known limitations explains what the sample does not prove and how to interpret results responsibly.
- Out of scope lists topics intentionally not covered by this repository.
Elapsed times are local demo observations. They are useful for understanding the shape of this sample topology, but they are not benchmark proof.
Timing can be affected by:
- local machine performance
- service warmup
- local HTTP overhead
- SQLite/local persistence behavior
- Orleans runtime behavior
- logging overhead
- gateway orchestration
- contention on one product identity
Use timings to ask better questions. Do not use them to claim universal performance superiority for either architecture style.
This project intentionally simplifies several production concerns:
- no production authentication or authorization model
- no full distributed tracing backend
- no production-grade payment integration
- no full deployment platform
- no multi-region or autoscaling design
- simplified persistence and migration strategy
- simplified timeout policy
- no intentionally unsafe race-condition scenario in the main comparison
See docs/17-known-limitations.md for the full interpretation guide and docs/18-out-of-scope.md for the explicit scope boundary.
The central comparison is not whether microservices or virtual actors are universally better.
The central comparison is how each style expresses and maintains:
- state ownership
- concurrency guarantees
- workflow coordination
- compensation policy
- idempotency behavior
- deployment and scaling boundaries
- operational diagnostics
- long-term evolution