orleans-multiservice-pattern

star 4

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

VincentH-Net By VincentH-Net schedule Updated 4/21/2026

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. TeamAeShopTeamA. The multiservice name is used in:

  • Solution name
  • Suffix for Apis and Contracts assembly 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:

  1. Dependency directions only: Apis.<S>Api → Contracts.<S>Contract, Apis.<S>Api → <S>Service, <S>Service → Contracts.<S>Contract. Nothing else.
  2. Namespace restrictions: API endpoints live in Apis.<service>Api; public grain contracts live in Contracts.<service>Contract; grain implementations live in <service>Service.
  3. 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).
  4. 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.Templates installed:
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.CatalogApi
  • Company.Product.Contracts.CatalogContract
  • Company.Product.CatalogService
  • Company.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-editorconfig in this solution — the bundled file supersedes it.
  • The .csproj flags documented in the dotnet-modern-csharp-editorconfig skill (Nullable=enable, TreatWarningsAsErrors=true, AnalysisLevel=preview-All, EnforceCodeStyleInBuild=true) still apply and are required for the bundled .editorconfig severities to take effect on dotnet build. Add them to Directory.Build.props at the solution root if not already present in the generated project files.
  • Rationale for every non-default setting is documented inline inside the bundled .editorconfig file.

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.BasketContractnot 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:

  1. Create a new multiservice containing only the service being extracted (e.g. TeamB containing Basket):

    dotnet new mcs-orleans-multiservice \
      --RootNamespace Company.Product \
      --Multiservice TeamB \
      --Logicalservice Basket \
      --allow-scripts Yes
    
  2. Remove the Basket logical service from the original multiservice.

  3. 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 from Apis.BasketApi) inside a thin adapter grain such as BasketServiceClientGrain. The rest of the calling code — which only saw the IBasketGrain contract 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>Contract project — grep the .csproj graph for illegal references.
  • <S>Service projects 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.ps1 step in a throwaway branch.

References

Install via CLI
npx skills add https://github.com/VincentH-Net/dotnet-agentic-engineering --skill orleans-multiservice-pattern
Repository Details
star Stars 4
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
VincentH-Net
VincentH-Net Explore all skills →