name: orleans-multiservice-pattern
description: Modular-monolith pattern for Microsoft Orleans 10 — host multiple logical services inside one physical silo/microservice, with project/namespace/dependency rules that let any logical service later be extracted to its own physical microservice with minimal changes. Use when starting a new Orleans 10 backend for a single team, when deferring real microservices until genuinely needed (MonolithFirst), when organizing a codebase to follow Conway's Law, or when you want painless future migration from in-silo grain calls to generated OpenAPI HTTP clients. Scaffolds Apis.<S>Api, Contracts.<S>Contract, and <S>Service projects with strict dependency directions (Apis→Contracts, Apis→Service, Service→Contracts), plus a silo host. Generated by the mcs-orleans-multiservice template; add more logical services via AddLogicalService.ps1 <name> or --Multiservice ..
metadata:
author: https://github.com/VincentH-Net
version: "1.0"
framework: orleans
category: architecture
sources:
- Modern.CSharp.Templates (mcs-orleans-multiservice)
- github.com/VincentH-Net/Orleans.Multiservice
Orleans Multiservice — logical service separation in a modular monolith
Use when building a Microsoft Orleans 10 backend and you want the productivity of a monolith (one deployable, in-process grain calls) with the optionality of microservices (any logical service can be extracted into its own physical microservice later, with minimal code changes). The pattern is a direct application of Martin Fowler's MonolithFirst and Conway's Law to Orleans.
1. When to use this skill
Apply when:
- Starting a new Orleans 10 backend owned by a single team, with synchronized deploys (sprint-based rhythm)
- You want clear functional boundaries in the code now but do not yet need the operational complexity of multiple services (separate CI/CD, infra, network hops)
- You anticipate needing to split services later (team growth, scaling isolation, independent deployment) and want the option to do so cheaply
- You want the compiler + project graph to enforce service boundaries today so they do not erode
Avoid when:
- You already need true physical isolation (independent deployability, polyglot stacks, team autonomy across services)
- Latency or failure-isolation requirements dictate separate processes from day one
2. Core concepts
Multiservice
A single deployable Orleans microservice (physical host / silo) that contains one or more logical services. Named for the team or product area, e.g. TeamA → eShopTeamA. The multiservice name is used in:
- Solution name
- Suffix for
ApisandContractsassembly names - Orleans cluster identity
Logical service
A functionally cohesive group of Orleans grains plus its related API endpoints, addressed exclusively through its public grain contracts. Examples: CatalogService, BasketService. Each logical service is three projects:
| Project | Public surface | Purpose |
|---|---|---|
Contracts.<Service>Contract |
Everything | Grain contract interfaces — the only intended public face of the service |
<Service>Service |
Restricted (see below) | Grain implementations |
Apis.<Service>Api |
Everything | ASP.NET Core endpoints exposing the service over HTTP |
In the grain implementation project (<Service>Service), public is allowed only for:
- Interface member implementations
- Grain constructors
- Serializable data members
This prevents other projects from calling service internals directly — they must go through grain contracts.
3. Structural rules
The project graph enforces four rules. Keep these invariants on every change:
- Dependency directions only:
Apis.<S>Api → Contracts.<S>Contract,Apis.<S>Api → <S>Service,<S>Service → Contracts.<S>Contract. Nothing else. - Namespace restrictions: API endpoints live in
Apis.<service>Api; public grain contracts live inContracts.<service>Contract; grain implementations live in<service>Service. - Cross-service isolation: No direct references between two services' namespaces or between two services' contract namespaces. A service that needs to call another service uses the other service's public grain contract (and, in the split-out case, the generated HTTP client).
- Public surface minimization: Service projects expose only grain implementations + constructors + serializable members. Helpers, DTOs, and internals stay
internal.
These rules are what make extraction cheap later. Breaking them pulls the cost of extraction back to the present.
4. Prerequisites
- .NET 10 SDK
- Microsoft Orleans 10 (the template targets 10; older Orleans versions are not supported by this skill)
- PowerShell 7+ (required for the post-action scripts — pass
--allow-scripts Yes) Modern.CSharp.Templatesinstalled:
dotnet new install Modern.CSharp.Templates
5. Create a new multiservice with its first logical service
dotnet new mcs-orleans-multiservice \
--RootNamespace Company.Product \
--Multiservice TeamA \
--Logicalservice Catalog \
--allow-scripts Yes
Required parameters
| Option | Purpose | Convention |
|---|---|---|
-R, --RootNamespace |
Prefix (no trailing dot) for every project's root namespace. | <Company>.<Product|Technology> |
-M, --Multiservice |
Name (without Service suffix) of the multiservice. Used in the solution name and as suffix for Apis.* and Contracts.* assembly names. |
[<Product|Technology>]<TeamName> |
-L, --Logicalservice |
Name (without Service suffix) of the first logical service to scaffold inside the multiservice. |
e.g. Catalog, Basket |
Optional parameters
| Option | Purpose | Default |
|---|---|---|
--allow-scripts |
Run post-action PowerShell scripts (needed for full project graph) | Prompt — pass Yes in CI |
-n, --name |
Output project root name | from --Multiservice |
-o, --output |
Output folder | current directory |
6. Generated solution layout
For --RootNamespace Company.Product --Multiservice TeamA --Logicalservice Catalog:
Company.Product.TeamA.sln
├── .editorconfig ← extended Modern C# 14 baseline + Orleans rules
├── src/
│ ├── Apis/
│ │ └── Apis.CatalogApi/ ← ASP.NET Core endpoints
│ ├── Contracts/
│ │ └── Contracts.CatalogContract/ ← public grain interfaces
│ ├── Services/
│ │ └── CatalogService/ ← grain implementations (restricted public)
│ └── Host/
│ └── TeamAHost/ ← Orleans silo + endpoint composition
├── AddLogicalService.ps1 ← script to add more logical services
└── ...
Namespaces follow the prefix + service pattern:
Company.Product.Apis.CatalogApiCompany.Product.Contracts.CatalogContractCompany.Product.CatalogServiceCompany.Product.TeamAHost
Bundled .editorconfig
The solution root includes an extended .editorconfig based on the mcs-editorconfig baseline (modern C# 14 formatting, naming, and preview-analyzer severities) with additional Orleans-specific rules layered on top. This means:
- Do not run
dotnet new mcs-editorconfigin this solution — the bundled file supersedes it. - The
.csprojflags documented in thedotnet-modern-csharp-editorconfigskill (Nullable=enable,TreatWarningsAsErrors=true,AnalysisLevel=preview-All,EnforceCodeStyleInBuild=true) still apply and are required for the bundled.editorconfigseverities to take effect ondotnet build. Add them toDirectory.Build.propsat the solution root if not already present in the generated project files. - Rationale for every non-default setting is documented inline inside the bundled
.editorconfigfile.
See the companion dotnet-modern-csharp-editorconfig skill for the baseline opinions (no underscore prefix on private fields, modern-idiom severities, etc.) — the bundled Orleans version inherits all of them.
7. Adding more logical services to an existing multiservice
Run the script generated alongside the solution:
.\AddLogicalService.ps1 Basket
This produces the three projects for the new logical service (Apis.BasketApi, Contracts.BasketContract, BasketService), registers them in the solution, and wires them into the existing host.
8. Service-to-service calls (inside the same multiservice)
A service that needs another service's data calls it via its grain contract — the same as any Orleans grain call:
// Inside CatalogService, calling BasketService
public class CatalogGrain(IGrainFactory grainFactory) : Grain, ICatalogGrain
{
public async Task AddToBasketAsync(string buyerId, int productId)
{
var basket = grainFactory.GetGrain<IBasketGrain>(buyerId);
await basket.AddItemAsync(productId);
}
}
This is an in-silo Orleans grain call — no HTTP, no serialization overhead (with [Immutable] on arguments).
Importantly: the calling service's project references Contracts.BasketContract — not BasketService. That is what keeps the extraction cheap later.
9. Splitting a logical service into its own microservice
When load, team ownership, or deploy cadence requires extracting a logical service:
Create a new multiservice containing only the service being extracted (e.g.
TeamBcontainingBasket):dotnet new mcs-orleans-multiservice \ --RootNamespace Company.Product \ --Multiservice TeamB \ --Logicalservice Basket \ --allow-scripts YesRemove the
Basketlogical service from the original multiservice.In services that still call
Basket(e.g.CatalogService), replace the in-silo grain-call path with a generated HTTP client (typically via OpenAPI spec fromApis.BasketApi) inside a thin adapter grain such asBasketServiceClientGrain. The rest of the calling code — which only saw theIBasketGraincontract interface — can often remain unchanged if the adapter implements the same contract.
Because step 3 is the only code change at the call site, extraction stays cheap — the structural rules from section 3 are what make this possible.
10. Design rationale
The pattern deliberately trades a small amount of up-front project overhead for two things:
- Deferred microservices cost — no multi-process infrastructure, no network hops, no independent-deploy coordination until you actually need them (MonolithFirst).
- Extractability as an invariant — the dependency rules + namespace rules + restricted public surface are enforced continuously. Without them, cross-service coupling accretes and extraction cost grows silently.
For the philosophical background, see Fowler's MonolithFirst and Conway's Law.
11. Checklist after adoption
- Solution structure matches section 6.
- Every reference between services goes through a
Contracts.<S>Contractproject — grep the.csprojgraph for illegal references. <S>Serviceprojects expose only what section 2 allows.- Orleans 10 is the effective runtime on the host; not a mixed-version setup.
- Adding a second logical service is a one-command operation, verified by running the
AddLogicalService.ps1step in a throwaway branch.
References
- Orleans.Multiservice repo — sample eShop solutions (single-team and two-team variants)
- Microsoft Orleans 10 docs
- Martin Fowler — MonolithFirst
- Martin Fowler — Conway's Law
- Building a Modular Monolith First