From 7f5f02459efc12a38410e4d6e4218b1dadcaf03d Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Thu, 10 Sep 2026 04:44:02 -0400 Subject: [PATCH 01/24] docs: add ADR 0035, service-oriented architecture Codifies the ten decisions that define what a service is and how it behaves under a service-oriented architecture: where boundaries come from, who owns a resource's data store, how a boundary is crossed, when a local copy is permitted, and what every service publishes. Recorded as Proposed so the decisions can be discussed and refined before ratification. Co-Authored-By: Claude Opus 5 (1M context) --- .../adr/0035-service-oriented-architecture.md | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 docs/architecture/adr/0035-service-oriented-architecture.md diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md new file mode 100644 index 000000000..39e929546 --- /dev/null +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -0,0 +1,206 @@ +--- +adr: "0035" +status: Proposed +date: 2026-09-10 +tags: [server, sdk] +--- + +# 0035 - Service-oriented architecture + + + +## Notation + +This ADR uses [RFC 2119](https://www.rfc-editor.org/info/rfc2119/) keywords (`MUST`, `MUST NOT`, +`SHOULD`, `SHOULD NOT`, `MAY`) deliberately. Anything marked `MUST` or `MUST NOT` is not negotiable +at team level; a team that believes it needs an exception brings the case to the architecture group +rather than adopting one unilaterally. + +**Service** here means an independently deployable process that owns a set of resources and +publishes a contract over them. **Consumer** means anything that calls a service — another service, +a user interface, or an external integration. **Service client** means the package a service +publishes so consumers can call it without hand-writing transport. **Row-level security** means the +rules an owning service applies to determine which rows a given caller may see — organization +scoping and any narrower per-user visibility the owner enforces, not tenancy alone. + +## Context and problem statement + +The server is one application over one shared data store. Any code path can join across any domain, +which means a domain's data has no owner in any enforceable sense: the schema is the integration +contract, and every team is coupled to every other team's tables. This is the constraint behind a +long tail of recurring problems — a change to one domain's schema cannot be reasoned about locally, +tenant isolation is applied by convention rather than by structure, and no team can deploy on its +own cadence. + +Tenant isolation is the clearest symptom. Organization scoping is currently enforced by roughly 98 +hand-written organization comparisons across 65 files, each applied to a row that has already been +read. Every one of them is a place a future change can omit the check, and nothing structural +distinguishes a correct call site from a missing one. + +The pieces needed to decompose already exist. `Bitwarden.Server.Sdk` is a shared MSBuild SDK package +consumed by fifteen projects in `server`. Command-query separation is established at scale per +[ADR-0008](./0008-server-CQRS-pattern.md), with several hundred single-operation command and query +classes already in the tree, and the newest service is factored into endpoints, handlers, and +commands. What is missing is not a mechanism but an agreement: a definition of what a service is, +what it owns, and how a boundary is crossed. Absent that, each extraction is negotiated from scratch +and the boundaries drift apart. + +Deciding nothing has a specific cost. Services will get extracted regardless, because teams need +independent deployment, and they will be extracted with divergent answers to the same questions — +who may read this table, what happens when the owner is unavailable, whether a copy is acceptable. +Reconciling those answers after the fact is far more expensive than agreeing them once. + +## Considered options + +- **Status quo:** one application over one shared store, with logical separation by convention. +- **Modular monolith:** enforce boundaries in code (module ownership, architecture tests) while + keeping the single store and single deployment. +- **Event-first services with local read models:** each service owns a store, and cross-boundary + reads are served from a local projection kept current by an event stream. +- **Service-oriented architecture:** each service owns a store, cross-boundary access goes through + the owner's published service client by default, and events propagate facts rather than carrying + the read path. + +### Status quo + +**Pros** + +- No migration cost, no version skew, no new operational surface. + +**Cons** + +- Does not deliver independent deployment, which is the requirement driving the work. +- Leaves tenant isolation as a convention applied at each call site. + +### Modular monolith + +**Pros** + +- Real boundary enforcement at compile time, at a fraction of the cost of extraction. +- No distributed-systems failure modes introduced. + +**Cons** + +- An architecture test cannot see SQL, so a module boundary does not stop a cross-domain join. +- Still one deployment, so cadence stays coupled. + +### Event-first services with local read models + +**Pros** + +- A consumer answers reads without depending on the owner being reachable. +- No synchronous call path to authenticate, authorize, or operate between services. + +**Cons** + +- A projection is a second implementation of the owner's read logic, including its tenant isolation, + which must be maintained in parallel and can diverge silently. +- Correctness becomes dependent on event delivery, on the tiers where the message transport is + weakest. +- The owner cannot enumerate, reach, or repair copies of its own data, so a representation defect + cannot be fixed centrally. + +### Service-oriented architecture + +**Pros** + +- One implementation of each read, owned by the team that owns the rules it enforces. +- Tenant isolation is enforced once, structurally, by the owner. +- Local copies remain available where they are genuinely warranted, as a recorded exception rather + than the default. + +**Cons** + +- Introduces a synchronous dependency between services, which must be authenticated, authorized, + cached, and operated. +- Independently deployable services are independently versioned services, which means compatibility + matrices on customer installations. + +## Decision outcome + +Chosen option: **service-oriented architecture**, because it keeps exactly one implementation of +each read under the ownership of the team accountable for its rules, and because putting a resource +behind exactly one owner is what makes tenant isolation enforceable in a single place rather than as +a convention repeated at every call site. + +The following decisions govern what a service is and how it behaves. + +1. Service boundaries `MUST` derive from data ownership, not from team structure. +2. Every resource `MUST` have exactly one owning service, and that service is the only process that + reads or writes its data store. +3. Services `MUST` be built on the `Bitwarden.Server.Sdk` package. +4. Services `MUST` document their APIs in [OpenAPI format](https://www.openapis.org/) and conform to + **API Standards**. +5. Services `MUST NOT` make breaking changes. +6. Services `MUST` provide a **service client** for clients and other services (a.k.a. "consumers") + to use. +7. Service clients `SHOULD` make use of a network cache to mitigate performance issues. + - Any cache used `MUST` be owned and invalidated by the owning service. + - Serving results from cache `MUST NOT` bypass authorization the owning service would otherwise + enforce. +8. Services that need to read, write, or validate data owned by another service `SHOULD` do so via + the service's published service client. +9. A service `MAY` hold a local copy of another service's data only with a recorded justification + (e.g. a measured hot-path volume, a stated availability requirement, etc.). + - Any service holding a local copy `MUST` enforce the owner's row-level security on that copy and + document the security ramifications of a stale copy (due to messaging lag, event processing + failures, etc.). +10. Services `MUST` publish events as a matter of course using the "transactional outbox" pattern, + regardless of whether there are any known consumers. + +### Positive consequences + +- A resource has one owner, so a schema change is reasoned about locally instead of across the + entire application. +- Decision 2 makes an owning service the only reader of its own store, which is what allows tenant + isolation to be enforced in one place instead of at every call site. The enforcement mechanism + itself is a service implementation standard rather than one of the decisions here. +- Teams deploy on their own cadence, since a consumer depends on a published contract rather than on + another team's build. +- Consumers write the same code on every deployment tier, because the service client resolves how a + call is made. +- Every state change is published, so audit and future integrations are fed from one stream that + already exists rather than from bespoke instrumentation added later. +- Local copies stay available for cases that genuinely warrant them, with the justification and the + staleness consequences written down where the copy is introduced. + +### Negative consequences + +- **Version skew becomes a supported condition.** Independently deployed services mean compatibility + matrices on customer installations, with no rollback available on a customer's own hardware. + Decision 5 is what keeps this tractable, and it is a permanent obligation rather than a migration + cost. +- **A synchronous dependency now exists where none did.** It has to be authenticated, authorized, + observed, and operated, and a dependency's unavailability becomes a caller's failure mode. +- **Service-to-service authentication has to be built for cloud.** The existing internal grant has + only ever been registered for self-hosted deployments, because cloud has had no service-to-service + path. Decomposition creates that path. +- **Every extracted service is another process on the smallest tier.** Bitwarden Lite already runs + nine processes on one box against an operator-supplied database, so the count of services is + priced there before anywhere else. +- **Row-level security in the data layer is not yet portable.** The current implementation composes + T-SQL and has no Entity Framework path. This affects structural tenant isolation generally — both + an owning service enforcing it over its own store and decision 9's requirement that a local copy + enforce it — so the isolation property this ADR relies on is available on SQL Server only until + that gap is closed. +- **Caching is not uniformly available.** Neither full self-host nor Bitwarden Lite ships a shared + cache today, so the cache in decision 7 is a cloud-only mechanism until that is addressed. + +### Plan + +- Publish **API Standards** as a companion page. Decision 4 references it normatively and it does + not exist yet on this site. +- Publish a client strategy page covering how a service client is generated, wrapped, versioned, and + cached, so decisions 6 and 7 have a single implementation pattern. +- Publish a service identity and context page covering service-to-service authentication, + authorization by scope, and context propagation, and build the cloud service-to-service path it + describes. +- Provide an Entity Framework path for organization scoping, so decision 9's row-level security + requirement holds on all supported database providers. +- Provide a transactional outbox and an event transport with an implementation that does not require + a message broker, so decision 10 holds on deployments that ship none. +- Decide the shared cache posture for full self-host and Bitwarden Lite, on which decision 7 depends + outside cloud. The cache implementation itself is already settled by + [ADR-0028](./0028-adopt-fusion-cache.md). +- Apply the decisions to the next service extraction as the reference implementation, and record + divergences here rather than in that service. From 210e65dfb1456907f34f53f116256ec946884f16 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Thu, 10 Sep 2026 12:22:20 -0400 Subject: [PATCH 02/24] Update service requirements in architecture document Clarified the breaking changes policy and API versioning process for services. --- .../adr/0035-service-oriented-architecture.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 39e929546..f4c6b94e4 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -131,21 +131,22 @@ The following decisions govern what a service is and how it behaves. 3. Services `MUST` be built on the `Bitwarden.Server.Sdk` package. 4. Services `MUST` document their APIs in [OpenAPI format](https://www.openapis.org/) and conform to **API Standards**. -5. Services `MUST NOT` make breaking changes. -6. Services `MUST` provide a **service client** for clients and other services (a.k.a. "consumers") +5. Services `MUST NOT` make breaking changes. Changes that *would* be breaking should follow the + API versioning process as outlined by API Standards. +7. Services `MUST` provide a **service client** for clients and other services (a.k.a. "consumers") to use. -7. Service clients `SHOULD` make use of a network cache to mitigate performance issues. +8. Service clients `SHOULD` make use of a network cache to mitigate performance issues. - Any cache used `MUST` be owned and invalidated by the owning service. - Serving results from cache `MUST NOT` bypass authorization the owning service would otherwise enforce. -8. Services that need to read, write, or validate data owned by another service `SHOULD` do so via +9. Services that need to read, write, or validate data owned by another service `SHOULD` do so via the service's published service client. -9. A service `MAY` hold a local copy of another service's data only with a recorded justification +10. A service `MAY` hold a local copy of another service's data only with a recorded justification (e.g. a measured hot-path volume, a stated availability requirement, etc.). - Any service holding a local copy `MUST` enforce the owner's row-level security on that copy and document the security ramifications of a stale copy (due to messaging lag, event processing failures, etc.). -10. Services `MUST` publish events as a matter of course using the "transactional outbox" pattern, +11. Services `MUST` publish events as a matter of course using the "transactional outbox" pattern, regardless of whether there are any known consumers. ### Positive consequences From fd7e5fdc96d5d9ec3b4883925aa7452b71a34d54 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Thu, 10 Sep 2026 12:23:28 -0400 Subject: [PATCH 03/24] Fix numbering and formatting in service architecture ADR --- .../adr/0035-service-oriented-architecture.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index f4c6b94e4..b8d054b70 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -133,20 +133,20 @@ The following decisions govern what a service is and how it behaves. **API Standards**. 5. Services `MUST NOT` make breaking changes. Changes that *would* be breaking should follow the API versioning process as outlined by API Standards. -7. Services `MUST` provide a **service client** for clients and other services (a.k.a. "consumers") +6. Services `MUST` provide a **service client** for clients and other services (a.k.a. "consumers") to use. -8. Service clients `SHOULD` make use of a network cache to mitigate performance issues. +7. Service clients `SHOULD` make use of a network cache to mitigate performance issues. - Any cache used `MUST` be owned and invalidated by the owning service. - Serving results from cache `MUST NOT` bypass authorization the owning service would otherwise enforce. -9. Services that need to read, write, or validate data owned by another service `SHOULD` do so via +8. Services that need to read, write, or validate data owned by another service `SHOULD` do so via the service's published service client. -10. A service `MAY` hold a local copy of another service's data only with a recorded justification +9. A service `MAY` hold a local copy of another service's data only with a recorded justification (e.g. a measured hot-path volume, a stated availability requirement, etc.). - Any service holding a local copy `MUST` enforce the owner's row-level security on that copy and document the security ramifications of a stale copy (due to messaging lag, event processing failures, etc.). -11. Services `MUST` publish events as a matter of course using the "transactional outbox" pattern, +10. Services `MUST` publish events as a matter of course using the "transactional outbox" pattern, regardless of whether there are any known consumers. ### Positive consequences From f11b26789aedadb9602a3c7d9b6b50b957c6a447 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Thu, 10 Sep 2026 12:31:01 -0400 Subject: [PATCH 04/24] Fix prettier formatting in decision 5 --- docs/architecture/adr/0035-service-oriented-architecture.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index b8d054b70..3c4127c8e 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -131,8 +131,8 @@ The following decisions govern what a service is and how it behaves. 3. Services `MUST` be built on the `Bitwarden.Server.Sdk` package. 4. Services `MUST` document their APIs in [OpenAPI format](https://www.openapis.org/) and conform to **API Standards**. -5. Services `MUST NOT` make breaking changes. Changes that *would* be breaking should follow the - API versioning process as outlined by API Standards. +5. Services `MUST NOT` make breaking changes. Changes that _would_ be breaking should follow the API + versioning process as outlined by API Standards. 6. Services `MUST` provide a **service client** for clients and other services (a.k.a. "consumers") to use. 7. Service clients `SHOULD` make use of a network cache to mitigate performance issues. From 758a17739603db4c4caa811e4fc37231422019d2 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Thu, 10 Sep 2026 12:33:31 -0400 Subject: [PATCH 05/24] Clarify API versioning requirement for services --- docs/architecture/adr/0035-service-oriented-architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 3c4127c8e..89a78ede7 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -131,7 +131,7 @@ The following decisions govern what a service is and how it behaves. 3. Services `MUST` be built on the `Bitwarden.Server.Sdk` package. 4. Services `MUST` document their APIs in [OpenAPI format](https://www.openapis.org/) and conform to **API Standards**. -5. Services `MUST NOT` make breaking changes. Changes that _would_ be breaking should follow the API +5. Services `MUST NOT` make breaking changes. Changes that _would_ be breaking `MUST` follow the API versioning process as outlined by API Standards. 6. Services `MUST` provide a **service client** for clients and other services (a.k.a. "consumers") to use. From e6910957a786977bcf85a9108f21df4a171e3505 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Fri, 11 Sep 2026 04:29:11 -0400 Subject: [PATCH 06/24] Update docs/architecture/adr/0035-service-oriented-architecture.md Co-authored-by: Mick Letofsky --- docs/architecture/adr/0035-service-oriented-architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 89a78ede7..a597030e2 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -2,7 +2,7 @@ adr: "0035" status: Proposed date: 2026-09-10 -tags: [server, sdk] +tags: [server, server-sdk] --- # 0035 - Service-oriented architecture From 782788d679e86f6796136db2854884016325d4e8 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Fri, 11 Sep 2026 05:28:20 -0400 Subject: [PATCH 07/24] Update ADR per feedback, externalize the SOA principles, rename server to service-oriented-architecture --- .../adr/0008-server-CQRS-pattern.md | 4 +- .../adr/0035-service-oriented-architecture.md | 159 ++++++++---------- docs/architecture/server/_category_.yml | 2 - docs/architecture/server/index.md | 24 --- .../_category_.yml | 2 + .../command-query-separation.md | 0 .../service-oriented-architecture/index.mdx | 36 ++++ .../model-separation-of-concerns.md | 0 .../service-oriented-architecture/services.md | 59 +++++++ 9 files changed, 172 insertions(+), 114 deletions(-) delete mode 100644 docs/architecture/server/_category_.yml delete mode 100644 docs/architecture/server/index.md create mode 100644 docs/architecture/service-oriented-architecture/_category_.yml rename docs/architecture/{server => service-oriented-architecture}/command-query-separation.md (100%) create mode 100644 docs/architecture/service-oriented-architecture/index.mdx rename docs/architecture/{server => service-oriented-architecture}/model-separation-of-concerns.md (100%) create mode 100644 docs/architecture/service-oriented-architecture/services.md diff --git a/docs/architecture/adr/0008-server-CQRS-pattern.md b/docs/architecture/adr/0008-server-CQRS-pattern.md index b4b69264c..2877a7a18 100644 --- a/docs/architecture/adr/0008-server-CQRS-pattern.md +++ b/docs/architecture/adr/0008-server-CQRS-pattern.md @@ -70,7 +70,7 @@ removed over time. ## Further reading -- [Command Query Separation](../server/command-query-separation.md) - Practical guide on - implementing CQS in the server codebase +- [Command Query Separation](../service-oriented-architecture/command-query-separation.md) - + Practical guide on implementing CQS in the server codebase - [Martin Fowler on CQS](https://martinfowler.com/bliki/CommandQuerySeparation.html) - High-level summary of the CQS principle diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 89a78ede7..dbb040c11 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -2,7 +2,7 @@ adr: "0035" status: Proposed date: 2026-09-10 -tags: [server, sdk] +tags: [server, server-sdk] --- # 0035 - Service-oriented architecture @@ -13,53 +13,53 @@ tags: [server, sdk] This ADR uses [RFC 2119](https://www.rfc-editor.org/info/rfc2119/) keywords (`MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, `MAY`) deliberately. Anything marked `MUST` or `MUST NOT` is not negotiable -at team level; a team that believes it needs an exception brings the case to the architecture group -rather than adopting one unilaterally. - -**Service** here means an independently deployable process that owns a set of resources and -publishes a contract over them. **Consumer** means anything that calls a service — another service, -a user interface, or an external integration. **Service client** means the package a service -publishes so consumers can call it without hand-writing transport. **Row-level security** means the -rules an owning service applies to determine which rows a given caller may see — organization -scoping and any narrower per-user visibility the owner enforces, not tenancy alone. +at team level; a team that needs an exception brings the case to the architecture group. The terms +_service_, _consumer_, _service client_, and _row-level security_ are defined in the +[service-oriented architecture standard](../service-oriented-architecture/services.md). ## Context and problem statement The server is one application over one shared data store. Any code path can join across any domain, -which means a domain's data has no owner in any enforceable sense: the schema is the integration -contract, and every team is coupled to every other team's tables. This is the constraint behind a -long tail of recurring problems — a change to one domain's schema cannot be reasoned about locally, -tenant isolation is applied by convention rather than by structure, and no team can deploy on its -own cadence. +so a domain's data has no enforceable owner: the schema is the integration contract, and every team +is coupled to every other team's tables. Three consequences follow: + + 1. A schema change cannot be reasoned about locally. + 2. Organization scoping is applied by convention at each call site. + 3. No team can deploy on its own cadence. -Tenant isolation is the clearest symptom. Organization scoping is currently enforced by roughly 98 -hand-written organization comparisons across 65 files, each applied to a row that has already been -read. Every one of them is a place a future change can omit the check, and nothing structural -distinguishes a correct call site from a missing one. +Organization scoping is the clearest symptom. It is enforced today by roughly 98 hand-written +organization comparisons across 65 files, each applied to a row that has already been read. Every +one is a place a future change can omit the check, and nothing structural distinguishes a correct +call site from a missing one. The pieces needed to decompose already exist. `Bitwarden.Server.Sdk` is a shared MSBuild SDK package consumed by fifteen projects in `server`. Command-query separation is established at scale per [ADR-0008](./0008-server-CQRS-pattern.md), with several hundred single-operation command and query -classes already in the tree, and the newest service is factored into endpoints, handlers, and -commands. What is missing is not a mechanism but an agreement: a definition of what a service is, -what it owns, and how a boundary is crossed. Absent that, each extraction is negotiated from scratch -and the boundaries drift apart. +classes in the tree. [ADR-0031](./0031-adopt-minimal-apis.md) and +[ADR-0032](./0032-break-up-core.md) already define the path a feature takes out of the monolith: a +feature-scoped library under `src/Libraries/[Feature]`, which moves to `src/Services/[Name]` when it +graduates into its own deployable container. What those decisions do not settle is what the +resulting service owns and how a boundary is crossed. + +Services will be extracted regardless, because teams need independent deployment. Without that +agreement they will be extracted with divergent answers to the same questions: + + - Who may read this table? + - What happens when the owner is unavailable? + - Is a copy is acceptable? -Deciding nothing has a specific cost. Services will get extracted regardless, because teams need -independent deployment, and they will be extracted with divergent answers to the same questions — -who may read this table, what happens when the owner is unavailable, whether a copy is acceptable. -Reconciling those answers after the fact is far more expensive than agreeing them once. +Reconciling those answers afterward is far more expensive than agreeing to them once. ## Considered options -- **Status quo:** one application over one shared store, with logical separation by convention. -- **Modular monolith:** enforce boundaries in code (module ownership, architecture tests) while +- **Status quo:** one monolithic application over one monolithic shared database, with logical + separation by convention. +- **Modular monolith:** enforce boundaries in code through module ownership and architecture tests, keeping the single store and single deployment. - **Event-first services with local read models:** each service owns a store, and cross-boundary reads are served from a local projection kept current by an event stream. - **Service-oriented architecture:** each service owns a store, cross-boundary access goes through - the owner's published service client by default, and events propagate facts rather than carrying - the read path. + the owner's published service client, and events propagate facts. ### Status quo @@ -70,14 +70,14 @@ Reconciling those answers after the fact is far more expensive than agreeing the **Cons** - Does not deliver independent deployment, which is the requirement driving the work. -- Leaves tenant isolation as a convention applied at each call site. +- Leaves organization scoping as a convention applied at each call site. ### Modular monolith **Pros** - Real boundary enforcement at compile time, at a fraction of the cost of extraction. -- No distributed-systems failure modes introduced. +- Introduces no distributed-systems failure modes. **Cons** @@ -93,9 +93,9 @@ Reconciling those answers after the fact is far more expensive than agreeing the **Cons** -- A projection is a second implementation of the owner's read logic, including its tenant isolation, - which must be maintained in parallel and can diverge silently. -- Correctness becomes dependent on event delivery, on the tiers where the message transport is +- A projection is a second implementation of the owner's read logic, including its row-level + security, and the two can diverge silently. +- Correctness depends on event delivery, including on the tiers where the message transport is weakest. - The owner cannot enumerate, reach, or repair copies of its own data, so a representation defect cannot be fixed centrally. @@ -105,9 +105,8 @@ Reconciling those answers after the fact is far more expensive than agreeing the **Pros** - One implementation of each read, owned by the team that owns the rules it enforces. -- Tenant isolation is enforced once, structurally, by the owner. -- Local copies remain available where they are genuinely warranted, as a recorded exception rather - than the default. +- Organization scoping is enforced once, by the owner. +- Local copies remain available where they are genuinely warranted, as a recorded exception. **Cons** @@ -118,12 +117,13 @@ Reconciling those answers after the fact is far more expensive than agreeing the ## Decision outcome -Chosen option: **service-oriented architecture**, because it keeps exactly one implementation of -each read under the ownership of the team accountable for its rules, and because putting a resource -behind exactly one owner is what makes tenant isolation enforceable in a single place rather than as -a convention repeated at every call site. +Chosen option: **service-oriented architecture** — one implementation of each read, owned by the +team accountable for its rules. -The following decisions govern what a service is and how it behaves. +The rules are published as the +[service-oriented architecture standard](../service-oriented-architecture/services.md). That page is +the living reference: its rules evolve by pull request without superseding this decision, and this +ADR is superseded only if the model itself changes. The rules at adoption: 1. Service boundaries `MUST` derive from data ownership, not from team structure. 2. Every resource `MUST` have exactly one owning service, and that service is the only process that @@ -133,8 +133,7 @@ The following decisions govern what a service is and how it behaves. **API Standards**. 5. Services `MUST NOT` make breaking changes. Changes that _would_ be breaking `MUST` follow the API versioning process as outlined by API Standards. -6. Services `MUST` provide a **service client** for clients and other services (a.k.a. "consumers") - to use. +6. Services `MUST` provide a **service client** for consumers. 7. Service clients `SHOULD` make use of a network cache to mitigate performance issues. - Any cache used `MUST` be owned and invalidated by the owning service. - Serving results from cache `MUST NOT` bypass authorization the owning service would otherwise @@ -146,62 +145,50 @@ The following decisions govern what a service is and how it behaves. - Any service holding a local copy `MUST` enforce the owner's row-level security on that copy and document the security ramifications of a stale copy (due to messaging lag, event processing failures, etc.). -10. Services `MUST` publish events as a matter of course using the "transactional outbox" pattern, +10. Services `MUST` publish events for every state change using the "transactional outbox" pattern, regardless of whether there are any known consumers. ### Positive consequences -- A resource has one owner, so a schema change is reasoned about locally instead of across the - entire application. -- Decision 2 makes an owning service the only reader of its own store, which is what allows tenant - isolation to be enforced in one place instead of at every call site. The enforcement mechanism - itself is a service implementation standard rather than one of the decisions here. -- Teams deploy on their own cadence, since a consumer depends on a published contract rather than on - another team's build. -- Consumers write the same code on every deployment tier, because the service client resolves how a - call is made. -- Every state change is published, so audit and future integrations are fed from one stream that - already exists rather than from bespoke instrumentation added later. -- Local copies stay available for cases that genuinely warrant them, with the justification and the - staleness consequences written down where the copy is introduced. +- A resource has one owner, so a schema change is reasoned about locally. +- A single owning process makes organization scoping enforceable in one place. +- Teams deploy on their own cadence against a published contract. +- Consumers write the same code on every deployment tier; the service client resolves how a call is + made. +- Audit and future integrations read one event stream that already exists. +- Local copies stay available where warranted, with the justification and the staleness consequences + recorded where the copy is introduced. ### Negative consequences - **Version skew becomes a supported condition.** Independently deployed services mean compatibility - matrices on customer installations, with no rollback available on a customer's own hardware. - Decision 5 is what keeps this tractable, and it is a permanent obligation rather than a migration - cost. + matrices on customer installations, with no rollback available on a customer's own hardware. Rule + 5 keeps this tractable, and it is a permanent obligation. - **A synchronous dependency now exists where none did.** It has to be authenticated, authorized, observed, and operated, and a dependency's unavailability becomes a caller's failure mode. - **Service-to-service authentication has to be built for cloud.** The existing internal grant has - only ever been registered for self-hosted deployments, because cloud has had no service-to-service - path. Decomposition creates that path. + only ever been registered for self-hosted deployments. - **Every extracted service is another process on the smallest tier.** Bitwarden Lite already runs - nine processes on one box against an operator-supplied database, so the count of services is - priced there before anywhere else. + nine processes on one box against an operator-supplied database, so service count is priced there + first. - **Row-level security in the data layer is not yet portable.** The current implementation composes - T-SQL and has no Entity Framework path. This affects structural tenant isolation generally — both - an owning service enforcing it over its own store and decision 9's requirement that a local copy - enforce it — so the isolation property this ADR relies on is available on SQL Server only until - that gap is closed. + T-SQL and has no Entity Framework path, so the enforcement this ADR relies on is available on SQL + Server only until that gap is closed. - **Caching is not uniformly available.** Neither full self-host nor Bitwarden Lite ships a shared - cache today, so the cache in decision 7 is a cloud-only mechanism until that is addressed. + cache today, so rule 7's cache is cloud-only until that is addressed. ### Plan -- Publish **API Standards** as a companion page. Decision 4 references it normatively and it does - not exist yet on this site. +- Publish **API Standards**. Rules 4 and 5 reference it normatively and it does not exist yet on + this site. - Publish a client strategy page covering how a service client is generated, wrapped, versioned, and - cached, so decisions 6 and 7 have a single implementation pattern. + cached. - Publish a service identity and context page covering service-to-service authentication, - authorization by scope, and context propagation, and build the cloud service-to-service path it - describes. -- Provide an Entity Framework path for organization scoping, so decision 9's row-level security - requirement holds on all supported database providers. -- Provide a transactional outbox and an event transport with an implementation that does not require - a message broker, so decision 10 holds on deployments that ship none. -- Decide the shared cache posture for full self-host and Bitwarden Lite, on which decision 7 depends - outside cloud. The cache implementation itself is already settled by - [ADR-0028](./0028-adopt-fusion-cache.md). -- Apply the decisions to the next service extraction as the reference implementation, and record - divergences here rather than in that service. + authorization by scope, and context propagation, and build the cloud path it describes. +- Provide an Entity Framework path for organization scoping, so rule 9 holds on all supported + database providers. +- Provide a transactional outbox and a broker-free event transport, so rule 10 holds on deployments + that ship no broker. +- Decide the shared cache posture for full self-host and Bitwarden Lite. The cache implementation is + settled by [ADR-0028](./0028-adopt-fusion-cache.md). +- Apply the standard to the next service extraction as the reference implementation. diff --git a/docs/architecture/server/_category_.yml b/docs/architecture/server/_category_.yml deleted file mode 100644 index f13944ff1..000000000 --- a/docs/architecture/server/_category_.yml +++ /dev/null @@ -1,2 +0,0 @@ -label: "Server Architecture" -position: 6 diff --git a/docs/architecture/server/index.md b/docs/architecture/server/index.md deleted file mode 100644 index a3b45be8c..000000000 --- a/docs/architecture/server/index.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -sidebar_position: 0 ---- - -# Server Architecture - -The Bitwarden server follows architectural patterns and conventions designed to maintain clean, -maintainable, and scalable code. - -## Key patterns - -### Command Query Separation (CQS) - -We use the CQS pattern to break up large service classes into smaller, focused commands and queries. -This results in classes with fewer interdependencies that are easier to change and test. - -See [Command Query Separation](command-query-separation.md) for details. - -### Model separation of concerns - -API contracts (request/response models) are kept separate from internal data models. This allows -APIs to evolve independently from internal data structures and business logic. - -See [Model separation of concerns](model-separation-of-concerns.md) for details. diff --git a/docs/architecture/service-oriented-architecture/_category_.yml b/docs/architecture/service-oriented-architecture/_category_.yml new file mode 100644 index 000000000..c70375c32 --- /dev/null +++ b/docs/architecture/service-oriented-architecture/_category_.yml @@ -0,0 +1,2 @@ +label: "Service-Oriented Architecture" +position: 6 diff --git a/docs/architecture/server/command-query-separation.md b/docs/architecture/service-oriented-architecture/command-query-separation.md similarity index 100% rename from docs/architecture/server/command-query-separation.md rename to docs/architecture/service-oriented-architecture/command-query-separation.md diff --git a/docs/architecture/service-oriented-architecture/index.mdx b/docs/architecture/service-oriented-architecture/index.mdx new file mode 100644 index 000000000..5b7e53d90 --- /dev/null +++ b/docs/architecture/service-oriented-architecture/index.mdx @@ -0,0 +1,36 @@ +--- +sidebar_position: 0 +--- + +# Service-Oriented Architecture + +**Audience:** Bitwarden engineers and AI agents building, extracting, or reviewing a service. + +This section is the living standard for how Bitwarden's services are built and how they relate to +one another: what a service owns, how a boundary is crossed, what travels on a call, and what every +service publishes. The service model was adopted in +[ADR-0035](../adr/0035-service-oriented-architecture.md), and the pages here evolve by pull request +without superseding that decision. + +## Key patterns + +### Services + +Service boundaries derive from data ownership. Every resource has exactly one owning service, and +consumers cross a boundary through the client that service publishes. + +See [Services](./services.md) for the full standard. + +### Command Query Separation (CQS) + +We use the CQS pattern to break up large service classes into smaller, focused commands and queries. +This results in classes with fewer interdependencies that are easier to change and test. + +See [Command Query Separation](./command-query-separation.md) for details. + +### Model separation of concerns + +API contracts (request/response models) are kept separate from internal data models. This allows +APIs to evolve independently from internal data structures and business logic. + +See [Model separation of concerns](./model-separation-of-concerns.md) for details. diff --git a/docs/architecture/server/model-separation-of-concerns.md b/docs/architecture/service-oriented-architecture/model-separation-of-concerns.md similarity index 100% rename from docs/architecture/server/model-separation-of-concerns.md rename to docs/architecture/service-oriented-architecture/model-separation-of-concerns.md diff --git a/docs/architecture/service-oriented-architecture/services.md b/docs/architecture/service-oriented-architecture/services.md new file mode 100644 index 000000000..75606bcb3 --- /dev/null +++ b/docs/architecture/service-oriented-architecture/services.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 3 +--- + +# Services + +**Audience:** Bitwarden engineers and AI agents building, extracting, or reviewing a server-side +service. + +This page is the living standard for how Bitwarden's services relate to one another: what a service +owns, how a boundary is crossed, and what every service publishes. It was adopted in +[ADR-0035](../adr/0035-service-oriented-architecture.md). The rules below evolve by pull request +without superseding that decision. + +[RFC 2119](https://www.rfc-editor.org/info/rfc2119/) keywords (`MUST`, `MUST NOT`, `SHOULD`, +`SHOULD NOT`, `MAY`) are used deliberately. A `MUST` or `MUST NOT` is not negotiable at team level; +a team that needs an exception brings the case to the architecture group. + +## Terms + +| Term | Definition | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Consumer** | Anything that calls a service: another service, a user interface, or an external integration. | +| **Row-level security** | The rules an owning service applies to decide which rows a caller may see, covering both organization scoping and any narrower per-user visibility. | +| **Service** | An independently deployable process that owns a set of resources and publishes a contract over them. | +| **Service client** | The package a service publishes so consumers can call it without hand-writing transport. | + +## Principles + +1. Service boundaries `MUST` derive from data ownership, not from team structure. +2. Every resource `MUST` have exactly one owning service, and that service is the only process that + reads or writes its data store. +3. Services `MUST` be built on the `Bitwarden.Server.Sdk` package. +4. Services `MUST` document their APIs in [OpenAPI format](https://www.openapis.org/) and conform to + **API Standards**. +5. Services `MUST NOT` make breaking changes. Changes that _would_ be breaking `MUST` follow the API + versioning process as outlined by API Standards. +6. Services `MUST` provide a **service client** for consumers. +7. Service clients `SHOULD` make use of a network cache to mitigate performance issues. + - Any cache used `MUST` be owned and invalidated by the owning service. + - Serving results from cache `MUST NOT` bypass authorization the owning service would otherwise + enforce. +8. Services that need to read, write, or validate data owned by another service `SHOULD` do so via + the service's published service client. +9. A service `MAY` hold a local copy of another service's data only with a recorded justification + (e.g. a measured hot-path volume, a stated availability requirement, etc.). + - Any service holding a local copy `MUST` enforce the owner's row-level security on that copy and + document the security ramifications of a stale copy (due to messaging lag, event processing + failures, etc.). +10. Services `MUST` publish events for every state change using the "transactional outbox" pattern, + regardless of whether there are any known consumers. + +## Related standards + +- [ADR-0008 Server: Adopt CQS](../adr/0008-server-CQRS-pattern.md) +- [ADR-0028 Adopt FusionCache](../adr/0028-adopt-fusion-cache.md) +- [ADR-0031 Adopt Minimal APIs](../adr/0031-adopt-minimal-apis.md) +- [ADR-0032 Break up the Core project](../adr/0032-break-up-core.md) +- [Command Query Separation (CQS)](./command-query-separation.md) From fee950fb148067a46fc16cc80a4461d905948ba2 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Fri, 11 Sep 2026 05:32:56 -0400 Subject: [PATCH 08/24] Fix list indentation and typo --- .../adr/0035-service-oriented-architecture.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index dbb040c11..ba9c91955 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -23,9 +23,9 @@ The server is one application over one shared data store. Any code path can join so a domain's data has no enforceable owner: the schema is the integration contract, and every team is coupled to every other team's tables. Three consequences follow: - 1. A schema change cannot be reasoned about locally. - 2. Organization scoping is applied by convention at each call site. - 3. No team can deploy on its own cadence. +1. A schema change cannot be reasoned about locally. +2. Organization scoping is applied by convention at each call site. +3. No team can deploy on its own cadence. Organization scoping is the clearest symptom. It is enforced today by roughly 98 hand-written organization comparisons across 65 files, each applied to a row that has already been read. Every @@ -44,9 +44,9 @@ resulting service owns and how a boundary is crossed. Services will be extracted regardless, because teams need independent deployment. Without that agreement they will be extracted with divergent answers to the same questions: - - Who may read this table? - - What happens when the owner is unavailable? - - Is a copy is acceptable? +- Who may read this table? +- What happens when the owner is unavailable? +- Is a copy acceptable? Reconciling those answers afterward is far more expensive than agreeing to them once. From cb54faf75eb6f51b539d64ce932b480cb95145dd Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Fri, 11 Sep 2026 05:52:52 -0400 Subject: [PATCH 09/24] Set ADR 0035 status to Accepted --- docs/architecture/adr/0035-service-oriented-architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index ba9c91955..60c7d513d 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -1,6 +1,6 @@ --- adr: "0035" -status: Proposed +status: Accepted date: 2026-09-10 tags: [server, server-sdk] --- From 228a6fdf8e05f26f51850703ba3371e27af635e3 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Fri, 11 Sep 2026 06:10:54 -0400 Subject: [PATCH 10/24] Strip out unnecessary context & problem statement. --- .../adr/0035-service-oriented-architecture.md | 32 +++---------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 60c7d513d..cf6a8a7b2 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -19,37 +19,14 @@ _service_, _consumer_, _service client_, and _row-level security_ are defined in ## Context and problem statement -The server is one application over one shared data store. Any code path can join across any domain, -so a domain's data has no enforceable owner: the schema is the integration contract, and every team -is coupled to every other team's tables. Three consequences follow: +The server is one monolithic application over one monolithic database. Any code path can join +across any domain, so a domain's data has no enforceable owner: the database is the integration +contract, and every team is coupled to every other team's tables. Three consequences follow: 1. A schema change cannot be reasoned about locally. 2. Organization scoping is applied by convention at each call site. 3. No team can deploy on its own cadence. -Organization scoping is the clearest symptom. It is enforced today by roughly 98 hand-written -organization comparisons across 65 files, each applied to a row that has already been read. Every -one is a place a future change can omit the check, and nothing structural distinguishes a correct -call site from a missing one. - -The pieces needed to decompose already exist. `Bitwarden.Server.Sdk` is a shared MSBuild SDK package -consumed by fifteen projects in `server`. Command-query separation is established at scale per -[ADR-0008](./0008-server-CQRS-pattern.md), with several hundred single-operation command and query -classes in the tree. [ADR-0031](./0031-adopt-minimal-apis.md) and -[ADR-0032](./0032-break-up-core.md) already define the path a feature takes out of the monolith: a -feature-scoped library under `src/Libraries/[Feature]`, which moves to `src/Services/[Name]` when it -graduates into its own deployable container. What those decisions do not settle is what the -resulting service owns and how a boundary is crossed. - -Services will be extracted regardless, because teams need independent deployment. Without that -agreement they will be extracted with divergent answers to the same questions: - -- Who may read this table? -- What happens when the owner is unavailable? -- Is a copy acceptable? - -Reconciling those answers afterward is far more expensive than agreeing to them once. - ## Considered options - **Status quo:** one monolithic application over one monolithic shared database, with logical @@ -117,8 +94,7 @@ Reconciling those answers afterward is far more expensive than agreeing to them ## Decision outcome -Chosen option: **service-oriented architecture** — one implementation of each read, owned by the -team accountable for its rules. +Chosen option: **Service-Oriented Architecture** The rules are published as the [service-oriented architecture standard](../service-oriented-architecture/services.md). That page is From 06a82c6a95a8f602c8522fc01afec8a72b0ded85 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Fri, 11 Sep 2026 06:27:09 -0400 Subject: [PATCH 11/24] Apply prettier to context paragraph --- docs/architecture/adr/0035-service-oriented-architecture.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index cf6a8a7b2..176b091c7 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -19,9 +19,9 @@ _service_, _consumer_, _service client_, and _row-level security_ are defined in ## Context and problem statement -The server is one monolithic application over one monolithic database. Any code path can join -across any domain, so a domain's data has no enforceable owner: the database is the integration -contract, and every team is coupled to every other team's tables. Three consequences follow: +The server is one monolithic application over one monolithic database. Any code path can join across +any domain, so a domain's data has no enforceable owner: the database is the integration contract, +and every team is coupled to every other team's tables. Three consequences follow: 1. A schema change cannot be reasoned about locally. 2. Organization scoping is applied by convention at each call site. From 9937d28ad45e2e62913e36ff08d93db06b4434f0 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 15 Sep 2026 05:53:49 -0400 Subject: [PATCH 12/24] Update the status of the ADR to proposed --- docs/architecture/adr/0035-service-oriented-architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 176b091c7..aa97bfb41 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -1,6 +1,6 @@ --- adr: "0035" -status: Accepted +status: Proposed date: 2026-09-10 tags: [server, server-sdk] --- From 5592b40f7af0e3958e3cb930f54e76d6b5c3521d Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 15 Sep 2026 05:54:22 -0400 Subject: [PATCH 13/24] Update date in ADR 0035 to 2026-09-22 --- docs/architecture/adr/0035-service-oriented-architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index aa97bfb41..6c22b09fe 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -1,7 +1,7 @@ --- adr: "0035" status: Proposed -date: 2026-09-10 +date: 2026-09-22 tags: [server, server-sdk] --- From 64da5450439ec512979f7964dca7b8eca847cfff Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 15 Sep 2026 07:19:44 -0400 Subject: [PATCH 14/24] Keep services.md under Server, revert rename --- .../adr/0008-server-CQRS-pattern.md | 4 +-- .../adr/0035-service-oriented-architecture.md | 9 +++-- docs/architecture/server/_category_.yml | 2 ++ .../command-query-separation.md | 0 docs/architecture/server/index.md | 24 +++++++++++++ .../model-separation-of-concerns.md | 0 .../services.md | 0 .../_category_.yml | 2 -- .../service-oriented-architecture/index.mdx | 36 ------------------- 9 files changed, 32 insertions(+), 45 deletions(-) create mode 100644 docs/architecture/server/_category_.yml rename docs/architecture/{service-oriented-architecture => server}/command-query-separation.md (100%) create mode 100644 docs/architecture/server/index.md rename docs/architecture/{service-oriented-architecture => server}/model-separation-of-concerns.md (100%) rename docs/architecture/{service-oriented-architecture => server}/services.md (100%) delete mode 100644 docs/architecture/service-oriented-architecture/_category_.yml delete mode 100644 docs/architecture/service-oriented-architecture/index.mdx diff --git a/docs/architecture/adr/0008-server-CQRS-pattern.md b/docs/architecture/adr/0008-server-CQRS-pattern.md index 2877a7a18..b4b69264c 100644 --- a/docs/architecture/adr/0008-server-CQRS-pattern.md +++ b/docs/architecture/adr/0008-server-CQRS-pattern.md @@ -70,7 +70,7 @@ removed over time. ## Further reading -- [Command Query Separation](../service-oriented-architecture/command-query-separation.md) - - Practical guide on implementing CQS in the server codebase +- [Command Query Separation](../server/command-query-separation.md) - Practical guide on + implementing CQS in the server codebase - [Martin Fowler on CQS](https://martinfowler.com/bliki/CommandQuerySeparation.html) - High-level summary of the CQS principle diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 6c22b09fe..6aa0429d8 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -15,7 +15,7 @@ This ADR uses [RFC 2119](https://www.rfc-editor.org/info/rfc2119/) keywords (`MU `SHOULD`, `SHOULD NOT`, `MAY`) deliberately. Anything marked `MUST` or `MUST NOT` is not negotiable at team level; a team that needs an exception brings the case to the architecture group. The terms _service_, _consumer_, _service client_, and _row-level security_ are defined in the -[service-oriented architecture standard](../service-oriented-architecture/services.md). +[service-oriented architecture standard](../server/services.md). ## Context and problem statement @@ -96,10 +96,9 @@ and every team is coupled to every other team's tables. Three consequences follo Chosen option: **Service-Oriented Architecture** -The rules are published as the -[service-oriented architecture standard](../service-oriented-architecture/services.md). That page is -the living reference: its rules evolve by pull request without superseding this decision, and this -ADR is superseded only if the model itself changes. The rules at adoption: +The rules are published as the [service-oriented architecture standard](../server/services.md). That +page is the living reference: its rules evolve by pull request without superseding this decision, +and this ADR is superseded only if the model itself changes. The rules at adoption: 1. Service boundaries `MUST` derive from data ownership, not from team structure. 2. Every resource `MUST` have exactly one owning service, and that service is the only process that diff --git a/docs/architecture/server/_category_.yml b/docs/architecture/server/_category_.yml new file mode 100644 index 000000000..f13944ff1 --- /dev/null +++ b/docs/architecture/server/_category_.yml @@ -0,0 +1,2 @@ +label: "Server Architecture" +position: 6 diff --git a/docs/architecture/service-oriented-architecture/command-query-separation.md b/docs/architecture/server/command-query-separation.md similarity index 100% rename from docs/architecture/service-oriented-architecture/command-query-separation.md rename to docs/architecture/server/command-query-separation.md diff --git a/docs/architecture/server/index.md b/docs/architecture/server/index.md new file mode 100644 index 000000000..a3b45be8c --- /dev/null +++ b/docs/architecture/server/index.md @@ -0,0 +1,24 @@ +--- +sidebar_position: 0 +--- + +# Server Architecture + +The Bitwarden server follows architectural patterns and conventions designed to maintain clean, +maintainable, and scalable code. + +## Key patterns + +### Command Query Separation (CQS) + +We use the CQS pattern to break up large service classes into smaller, focused commands and queries. +This results in classes with fewer interdependencies that are easier to change and test. + +See [Command Query Separation](command-query-separation.md) for details. + +### Model separation of concerns + +API contracts (request/response models) are kept separate from internal data models. This allows +APIs to evolve independently from internal data structures and business logic. + +See [Model separation of concerns](model-separation-of-concerns.md) for details. diff --git a/docs/architecture/service-oriented-architecture/model-separation-of-concerns.md b/docs/architecture/server/model-separation-of-concerns.md similarity index 100% rename from docs/architecture/service-oriented-architecture/model-separation-of-concerns.md rename to docs/architecture/server/model-separation-of-concerns.md diff --git a/docs/architecture/service-oriented-architecture/services.md b/docs/architecture/server/services.md similarity index 100% rename from docs/architecture/service-oriented-architecture/services.md rename to docs/architecture/server/services.md diff --git a/docs/architecture/service-oriented-architecture/_category_.yml b/docs/architecture/service-oriented-architecture/_category_.yml deleted file mode 100644 index c70375c32..000000000 --- a/docs/architecture/service-oriented-architecture/_category_.yml +++ /dev/null @@ -1,2 +0,0 @@ -label: "Service-Oriented Architecture" -position: 6 diff --git a/docs/architecture/service-oriented-architecture/index.mdx b/docs/architecture/service-oriented-architecture/index.mdx deleted file mode 100644 index 5b7e53d90..000000000 --- a/docs/architecture/service-oriented-architecture/index.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -sidebar_position: 0 ---- - -# Service-Oriented Architecture - -**Audience:** Bitwarden engineers and AI agents building, extracting, or reviewing a service. - -This section is the living standard for how Bitwarden's services are built and how they relate to -one another: what a service owns, how a boundary is crossed, what travels on a call, and what every -service publishes. The service model was adopted in -[ADR-0035](../adr/0035-service-oriented-architecture.md), and the pages here evolve by pull request -without superseding that decision. - -## Key patterns - -### Services - -Service boundaries derive from data ownership. Every resource has exactly one owning service, and -consumers cross a boundary through the client that service publishes. - -See [Services](./services.md) for the full standard. - -### Command Query Separation (CQS) - -We use the CQS pattern to break up large service classes into smaller, focused commands and queries. -This results in classes with fewer interdependencies that are easier to change and test. - -See [Command Query Separation](./command-query-separation.md) for details. - -### Model separation of concerns - -API contracts (request/response models) are kept separate from internal data models. This allows -APIs to evolve independently from internal data structures and business logic. - -See [Model separation of concerns](./model-separation-of-concerns.md) for details. From 8a537f03b6e01945d4c2869e656e25cdd84ac37c Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 15 Sep 2026 07:22:11 -0400 Subject: [PATCH 15/24] Remove services.md --- docs/architecture/server/services.md | 59 ---------------------------- 1 file changed, 59 deletions(-) delete mode 100644 docs/architecture/server/services.md diff --git a/docs/architecture/server/services.md b/docs/architecture/server/services.md deleted file mode 100644 index 75606bcb3..000000000 --- a/docs/architecture/server/services.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Services - -**Audience:** Bitwarden engineers and AI agents building, extracting, or reviewing a server-side -service. - -This page is the living standard for how Bitwarden's services relate to one another: what a service -owns, how a boundary is crossed, and what every service publishes. It was adopted in -[ADR-0035](../adr/0035-service-oriented-architecture.md). The rules below evolve by pull request -without superseding that decision. - -[RFC 2119](https://www.rfc-editor.org/info/rfc2119/) keywords (`MUST`, `MUST NOT`, `SHOULD`, -`SHOULD NOT`, `MAY`) are used deliberately. A `MUST` or `MUST NOT` is not negotiable at team level; -a team that needs an exception brings the case to the architecture group. - -## Terms - -| Term | Definition | -| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Consumer** | Anything that calls a service: another service, a user interface, or an external integration. | -| **Row-level security** | The rules an owning service applies to decide which rows a caller may see, covering both organization scoping and any narrower per-user visibility. | -| **Service** | An independently deployable process that owns a set of resources and publishes a contract over them. | -| **Service client** | The package a service publishes so consumers can call it without hand-writing transport. | - -## Principles - -1. Service boundaries `MUST` derive from data ownership, not from team structure. -2. Every resource `MUST` have exactly one owning service, and that service is the only process that - reads or writes its data store. -3. Services `MUST` be built on the `Bitwarden.Server.Sdk` package. -4. Services `MUST` document their APIs in [OpenAPI format](https://www.openapis.org/) and conform to - **API Standards**. -5. Services `MUST NOT` make breaking changes. Changes that _would_ be breaking `MUST` follow the API - versioning process as outlined by API Standards. -6. Services `MUST` provide a **service client** for consumers. -7. Service clients `SHOULD` make use of a network cache to mitigate performance issues. - - Any cache used `MUST` be owned and invalidated by the owning service. - - Serving results from cache `MUST NOT` bypass authorization the owning service would otherwise - enforce. -8. Services that need to read, write, or validate data owned by another service `SHOULD` do so via - the service's published service client. -9. A service `MAY` hold a local copy of another service's data only with a recorded justification - (e.g. a measured hot-path volume, a stated availability requirement, etc.). - - Any service holding a local copy `MUST` enforce the owner's row-level security on that copy and - document the security ramifications of a stale copy (due to messaging lag, event processing - failures, etc.). -10. Services `MUST` publish events for every state change using the "transactional outbox" pattern, - regardless of whether there are any known consumers. - -## Related standards - -- [ADR-0008 Server: Adopt CQS](../adr/0008-server-CQRS-pattern.md) -- [ADR-0028 Adopt FusionCache](../adr/0028-adopt-fusion-cache.md) -- [ADR-0031 Adopt Minimal APIs](../adr/0031-adopt-minimal-apis.md) -- [ADR-0032 Break up the Core project](../adr/0032-break-up-core.md) -- [Command Query Separation (CQS)](./command-query-separation.md) From e880d2d9960a5e4d1f831249cfe1ed20db4ac04f Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 15 Sep 2026 07:23:17 -0400 Subject: [PATCH 16/24] Remove links to deleted services.md --- .../adr/0035-service-oriented-architecture.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 6aa0429d8..112d97689 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -13,9 +13,7 @@ tags: [server, server-sdk] This ADR uses [RFC 2119](https://www.rfc-editor.org/info/rfc2119/) keywords (`MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, `MAY`) deliberately. Anything marked `MUST` or `MUST NOT` is not negotiable -at team level; a team that needs an exception brings the case to the architecture group. The terms -_service_, _consumer_, _service client_, and _row-level security_ are defined in the -[service-oriented architecture standard](../server/services.md). +at team level; a team that needs an exception brings the case to the architecture group. ## Context and problem statement @@ -96,9 +94,7 @@ and every team is coupled to every other team's tables. Three consequences follo Chosen option: **Service-Oriented Architecture** -The rules are published as the [service-oriented architecture standard](../server/services.md). That -page is the living reference: its rules evolve by pull request without superseding this decision, -and this ADR is superseded only if the model itself changes. The rules at adoption: +The rules: 1. Service boundaries `MUST` derive from data ownership, not from team structure. 2. Every resource `MUST` have exactly one owning service, and that service is the only process that From 717b124511e5b6bbe553e3a628f7b098df7bda16 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 15 Sep 2026 08:47:59 -0400 Subject: [PATCH 17/24] Apply review wording suggestions --- .../adr/0035-service-oriented-architecture.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 112d97689..cc11ea27d 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -18,8 +18,8 @@ at team level; a team that needs an exception brings the case to the architectur ## Context and problem statement The server is one monolithic application over one monolithic database. Any code path can join across -any domain, so a domain's data has no enforceable owner: the database is the integration contract, -and every team is coupled to every other team's tables. Three consequences follow: +domain boundaries, leaving data with no enforceable owner. The database serves as the integration +contract, coupling teams directly to one another's tables. Three consequences follow: 1. A schema change cannot be reasoned about locally. 2. Organization scoping is applied by convention at each call site. @@ -110,11 +110,11 @@ The rules: - Serving results from cache `MUST NOT` bypass authorization the owning service would otherwise enforce. 8. Services that need to read, write, or validate data owned by another service `SHOULD` do so via - the service's published service client. + the owner's published service client. 9. A service `MAY` hold a local copy of another service's data only with a recorded justification (e.g. a measured hot-path volume, a stated availability requirement, etc.). - - Any service holding a local copy `MUST` enforce the owner's row-level security on that copy and - document the security ramifications of a stale copy (due to messaging lag, event processing + - Any service holding a local copy `MUST` enforce the owner's row-level security on that data and + document the security ramifications of stale reads (due to messaging lag, event processing failures, etc.). 10. Services `MUST` publish events for every state change using the "transactional outbox" pattern, regardless of whether there are any known consumers. @@ -136,7 +136,7 @@ The rules: matrices on customer installations, with no rollback available on a customer's own hardware. Rule 5 keeps this tractable, and it is a permanent obligation. - **A synchronous dependency now exists where none did.** It has to be authenticated, authorized, - observed, and operated, and a dependency's unavailability becomes a caller's failure mode. + observed, and operated. A dependency's unavailability becomes a caller's failure mode. - **Service-to-service authentication has to be built for cloud.** The existing internal grant has only ever been registered for self-hosted deployments. - **Every extracted service is another process on the smallest tier.** Bitwarden Lite already runs From 15feec7512882a9e30183f6d471d1ebd37d6337e Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 15 Sep 2026 14:29:34 -0400 Subject: [PATCH 18/24] Update "Versioning" negative consequence --- .../adr/0035-service-oriented-architecture.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index cc11ea27d..8baa6ce2f 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -87,8 +87,8 @@ contract, coupling teams directly to one another's tables. Three consequences fo - Introduces a synchronous dependency between services, which must be authenticated, authorized, cached, and operated. -- Independently deployable services are independently versioned services, which means compatibility - matrices on customer installations. +- Independently deployable services are independently versioned services, which the release pipeline + has to keep shipping as one coordinated set. ## Decision outcome @@ -132,9 +132,12 @@ The rules: ### Negative consequences -- **Version skew becomes a supported condition.** Independently deployed services mean compatibility - matrices on customer installations, with no rollback available on a customer's own hardware. Rule - 5 keeps this tractable, and it is a permanent obligation. +- **Versioning.** Independently deployable services will likely be versioned independently as well, + and enhancements and bug fixes will land service by service, each producing a new version of that + service. Rule 5 ensures a new version never introduces a breaking change, so a customer may at any + time run an "upgrade everything" script and take the latest of every service — which is exactly + what self-host installs. Our obligation is to just make sure "the latest version of everything" + always works. - **A synchronous dependency now exists where none did.** It has to be authenticated, authorized, observed, and operated. A dependency's unavailability becomes a caller's failure mode. - **Service-to-service authentication has to be built for cloud.** The existing internal grant has From 2eb20b6cfd3f6d416afd2f629d848357ed49abaa Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 15 Sep 2026 14:57:31 -0400 Subject: [PATCH 19/24] Update docs/architecture/adr/0035-service-oriented-architecture.md Co-authored-by: Matt Bishop --- docs/architecture/adr/0035-service-oriented-architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 8baa6ce2f..103642e52 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -92,7 +92,7 @@ contract, coupling teams directly to one another's tables. Three consequences fo ## Decision outcome -Chosen option: **Service-Oriented Architecture** +Chosen option: **Service-oriented architecture**. The rules: From 48be7c57a42bd3cee15738a2f582b42d6023beea Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 15 Sep 2026 15:18:16 -0400 Subject: [PATCH 20/24] Update positive & negative consequences --- .../adr/0035-service-oriented-architecture.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 8baa6ce2f..b598b92e5 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -25,6 +25,12 @@ contract, coupling teams directly to one another's tables. Three consequences fo 2. Organization scoping is applied by convention at each call site. 3. No team can deploy on its own cadence. +Every team ships on the monolith's schedule and any regression anywhere blocks everyone. As a +result, teams may respond by batching work into larger releases, which makes each release riskier to +review and harder to roll back. Independent deployment is what breaks that cycle: a team that owns +its store, its service, and its release can make whatever changes they need to make, whenever they +need to make them, without it becoming a coordinated effort. + ## Considered options - **Status quo:** one monolithic application over one monolithic shared database, with logical @@ -87,6 +93,10 @@ contract, coupling teams directly to one another's tables. Three consequences fo - Introduces a synchronous dependency between services, which must be authenticated, authorized, cached, and operated. +- Adds latency to any read that crosses a boundary and can be particularly harmful if 1 API + invocation turns into N calls to another service. This, however, is not unlike from N+1 database + queries that can result from a careless for loop and the same strategies used to turn N+1 database + queries into 2 queries can usually be brought to bear for service-to-service calls, as well. - Independently deployable services are independently versioned services, which the release pipeline has to keep shipping as one coordinated set. @@ -148,8 +158,10 @@ The rules: - **Row-level security in the data layer is not yet portable.** The current implementation composes T-SQL and has no Entity Framework path, so the enforcement this ADR relies on is available on SQL Server only until that gap is closed. -- **Caching is not uniformly available.** Neither full self-host nor Bitwarden Lite ships a shared - cache today, so rule 7's cache is cloud-only until that is addressed. +- **A cache cannot be assumed to exist.** Redis and Cosmos are both exposed through configuration + and some self-host operators do configure one, but neither is provisioned by default and Bitwarden + Lite ships no cache service at all. Rule 7's caching is therefore an optimization a service may + find available, never a mechanism it can depend on being there. ### Plan From 07494bbdefff64ea29ca96dff5894046d5d0a748 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Tue, 15 Sep 2026 15:33:24 -0400 Subject: [PATCH 21/24] Fix grammar --- docs/architecture/adr/0035-service-oriented-architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 58860bbcf..5dc105aa6 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -94,7 +94,7 @@ need to make them, without it becoming a coordinated effort. - Introduces a synchronous dependency between services, which must be authenticated, authorized, cached, and operated. - Adds latency to any read that crosses a boundary and can be particularly harmful if 1 API - invocation turns into N calls to another service. This, however, is not unlike from N+1 database + invocation turns into N calls to another service. This, however, is not unlike N+1 database queries that can result from a careless for loop and the same strategies used to turn N+1 database queries into 2 queries can usually be brought to bear for service-to-service calls, as well. - Independently deployable services are independently versioned services, which the release pipeline From 8bfcfedc6fd5233c8ffc0ac2d55af1d50159f0b8 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Wed, 16 Sep 2026 06:04:07 -0400 Subject: [PATCH 22/24] Clarify event standards is out of scope of this ADR. --- docs/architecture/adr/0035-service-oriented-architecture.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 5dc105aa6..991979fc4 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -127,7 +127,8 @@ The rules: document the security ramifications of stale reads (due to messaging lag, event processing failures, etc.). 10. Services `MUST` publish events for every state change using the "transactional outbox" pattern, - regardless of whether there are any known consumers. + regardless of whether there are any known consumers. Standards for these events will be the + subject of a forthcoming ADR and out of scope for this ADR. ### Positive consequences From 1d07899e178f1d561e3dbc2a86f78cea6668b3e6 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Fri, 18 Sep 2026 13:39:39 -0400 Subject: [PATCH 23/24] Make it clear that cascade deletes are implemented by listening to events --- .../adr/0035-service-oriented-architecture.md | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 991979fc4..642738c47 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -127,8 +127,18 @@ The rules: document the security ramifications of stale reads (due to messaging lag, event processing failures, etc.). 10. Services `MUST` publish events for every state change using the "transactional outbox" pattern, - regardless of whether there are any known consumers. Standards for these events will be the - subject of a forthcoming ADR and out of scope for this ADR. + regardless of whether there are any known consumers. +11. A service that owns resources whose lifetime depends on a resource owned by another service + `MUST` consume that owner's "resource deleted" events and cascade the deletion to the resources + it owns. An owning service is not responsible for deleting data it does not own. + +:::note + +Standards for events published and consumed by services, including the "shape" of these events, the +authorization model, and retry and dead-letter policies, will be the subject of a forthcoming ADR +and are out of scope here. + +::: ### Positive consequences @@ -138,6 +148,9 @@ The rules: - Consumers write the same code on every deployment tier; the service client resolves how a call is made. - Audit and future integrations read one event stream that already exists. +- A data owner does not need to know which services hold data that depends on its resources. + Dependents invert the dependency by subscribing to the owner's events, so adding a dependent + requires no change to the owner. - Local copies stay available where warranted, with the justification and the staleness consequences recorded where the copy is introduced. @@ -174,8 +187,8 @@ The rules: authorization by scope, and context propagation, and build the cloud path it describes. - Provide an Entity Framework path for organization scoping, so rule 9 holds on all supported database providers. -- Provide a transactional outbox and a broker-free event transport, so rule 10 holds on deployments - that ship no broker. +- Provide a transactional outbox and a broker-free event transport, so rules 10 through 12 hold on + deployments that ship no broker. - Decide the shared cache posture for full self-host and Bitwarden Lite. The cache implementation is settled by [ADR-0028](./0028-adopt-fusion-cache.md). - Apply the standard to the next service extraction as the reference implementation. From ecb9f3c5822977a43360fec4489cab332dec1369 Mon Sep 17 00:00:00 2001 From: Mike Gorman Date: Fri, 18 Sep 2026 13:52:00 -0400 Subject: [PATCH 24/24] Fix rule reference --- docs/architecture/adr/0035-service-oriented-architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr/0035-service-oriented-architecture.md b/docs/architecture/adr/0035-service-oriented-architecture.md index 642738c47..5ab210a89 100644 --- a/docs/architecture/adr/0035-service-oriented-architecture.md +++ b/docs/architecture/adr/0035-service-oriented-architecture.md @@ -187,7 +187,7 @@ and are out of scope here. authorization by scope, and context propagation, and build the cloud path it describes. - Provide an Entity Framework path for organization scoping, so rule 9 holds on all supported database providers. -- Provide a transactional outbox and a broker-free event transport, so rules 10 through 12 hold on +- Provide a transactional outbox and a broker-free event transport, so rules 10 and 11 hold on deployments that ship no broker. - Decide the shared cache posture for full self-host and Bitwarden Lite. The cache implementation is settled by [ADR-0028](./0028-adopt-fusion-cache.md).