TLDR
- Before: three giant classes did everything data-related:
DataManager(3,266 lines, 16 ports),RestService(2,323 lines, hand-rolled HTTP + 4 generated clients) andAuthentication(1,280 lines, session + tokens + credentials + logout side effects).- After: each concern has one home. A port in
ALPAMobile.Application, an implementation inALPAMobile.Infrastructure, one shared transport, one shared cache helper, and four narrow auth ports.DataManagerandRestServiceare deleted.- What did not change: the 16 query ports consumers already inject, every cache key, every persisted JSON shape, and every route. Callers and installed caches are unaffected.
- What is new and load-bearing: a cache generation that stops a request started by one member from writing into the next member's session (logout and account-switch safety).
- Status: delivered in eight phases (0–7) plus a review-remediation pass. It ships as one integration PR, PR 2543 (
imp/blazor-hybrid-service-layer-decoupling→imp/blazor-hybrid), whose logged-out Home paint hold was corrected on 2026-09-23 (see Status).
Read top to bottom and stop when you have enough. Each level adds detail; none repeats the level above.
| Level | You get | Read if you… |
|---|---|---|
| 1 | The 2-minute picture | are reviewing PR 2543 or onboarding |
| 2 | How a request flows | are changing or adding a service |
| 3 | The building blocks | need the exact type and its rule |
| 4 | Phase by phase | want to know what landed where, and why |
| 5 | Recipes and guardrails | are about to write code in this area |
| 6 | Status and reference docs | need evidence, ledgers, or open gates |
Every domain (MEC, documents, notifications, …) re-implemented the same block by hand inside DataManager: check connectivity → check auth → call RestService → save to cache → on failure read stale cache. HTTP, retry, bearer-token handling and even UI snackbars lived in RestService. Login, logout and token refresh lived in Authentication, which also reached into the cache and theme/feed on logout.
Consequences: no unit could be tested alone, the head (MAUI app) project owned logic that has no MAUI dependency, and a slow response arriving after logout could repopulate the cache with the previous member's data.
Consumers (ViewModels, Razor pages)
│ inject narrow ports — unchanged names, e.g. IMECQueries, IDocumentsQueries
▼
ALPAMobile.Application ports + orchestration (CacheContentService, prefetch)
│
▼
ALPAMobile.Infrastructure one focused service per domain (MecService, DocumentQueries, …)
│ per-domain API client → one shared transport → CachedRemoteQuery → ICacheStore
▼
Head (ALPAMobile) only what needs MAUI: navigation, native file/viewer, push, connectivity
| # | Idea | One-line meaning |
|---|---|---|
| 1 | One home per concern | A domain service in Infrastructure behind the port it already exposed. |
| 2 | One transport | Every JSON call goes through IRequestProvider; one place owns retry, timeout, 401 refresh. |
| 3 | One cache pattern | CachedRemoteQuery replaces the copy-pasted fetch/cache/fallback blocks. |
| 4 | Generation guard | Work started under an old session cannot write memory, disk, or Search rows. |
| 5 | Behavior preserved on purpose | Keys, JSON shapes, routes and per-domain fallback rules are pinned by tests. |
Example: a signed-in member opens the MEC committees list.
IMECQueries (an Application port). It has never known who implements it.MecService singleton (formerly forwarding code in DataManager).MecService asks CachedRemoteQuery for the data, naming the cache key (GetMECCommitteesAsync, unchanged), a codec (legacy Newtonsoft shape) and a policy.CachedRemoteQuery captures the current cache generation, then calls the MEC API client.401 → one forced token refresh, one replay;Paths are relative to the repository root.
| Type | Location | Rule |
|---|---|---|
IRequestProvider / RequestProvider |
ALPAMobile.Infrastructure/Remote/RequestProvider.cs |
Raw JSON transport returning Result<T>. Used directly for token exchange, which must not be decorated. |
AuthenticatedRequestProvider |
ALPAMobile.Infrastructure/Remote/AuthenticatedRequestProvider.cs |
Decorator that adds bearer tokens, retries, and one 401 replay. |
ApiEndpointOptions |
ALPAMobile.Application/Abstractions/Remote/ApiEndpointOptions.cs |
Single source of truth for hosts, validated on start. Replaces RestService.SetEndpoints(). |
IApiEndpointCoordinator |
ALPAMobile.Application/Abstractions/Remote/ |
Settings surfaces publish environment changes through it; clients read the current snapshot per request. |
Transport defaults (from the implementation evidence):
| Setting | Value |
|---|---|
| Sends per GET | 3 (MaxRetries = 2) |
| Per-attempt timeout | 15 s (includes body read) |
| Backoff | 2 s then 4 s, plus jitter |
| Total budget | 60 s, covering backoff, token acquisition and the 401 replay |
DocumentsApiClient |
30 s per attempt, 90 s total (getdocuments can take 30 s) |
| JWT exchange | 1 retry, transport failure only (a lost reply to a rejected password still counts toward lockout) |
| Ordinary writes | sent once |
Replay-safe exceptions that retry despite being non-GET: notification list reads, push-registration upserts, document download reads, flight-search read POSTs, favorite DELETEs.
| Type | Location | Rule |
|---|---|---|
ICacheStore |
ALPAMobile.Application/Abstractions/Caching/ICacheStore.cs |
Port over the LiteDB cache (CacheDatabase implements it). Keys and JSON format unchanged. |
CacheContextCoordinator |
ALPAMobile.Infrastructure/Caching/CacheContextCoordinator.cs |
Owns the generation. Writes run inside TryExecute with the generation captured before the fetch. |
CachedRemoteQuery |
ALPAMobile.Infrastructure/Caching/CachedRemoteQuery.cs |
Takes an explicit key, codec and policy. Reports live / memory / persisted / missing. Shares in-flight work between callers. |
*CacheCodecs |
ALPAMobile.Infrastructure/Caching/ |
Read and write the legacy persisted shape. Domain shape is not the wire shape. |
Two named policies replace the old copy-pasted shapes: fallback on transport failure and stale-while-revalidate (documents).
Two hard constraints, both pinned by tests:
SearchContentRegistry.Keys). Changing one silently blanks Search and the on-disk cache after upgrade.One focused service per domain, all in ALPAMobile.Infrastructure, resolved through the existing ports:
| Domain | Implementation |
|---|---|
| MEC / LEC | Services/Mec/MecService |
| Content, Member, Jumpseat/KCM, Notifications, Subscriptions, FlightSearch | Services/* |
| Documents | Services/Documents/DocumentQueries (facade over catalogue and local store) |
| Menu, Favorites | Services/MenuService, Services/FavoritesService |
| FTDT reads | Services/FlightTimeDutyTime/FtdtQueries, over IFTDTDutyPeriodRepository |
Prefetch orchestration (CacheContentService) lives in Application (Services/Caching/). It talks to platform concerns only through ports.
IAuthentication (24 members) is split into four ports, all aliased to the one Authentication singleton:
| Port | Responsibility |
|---|---|
ISessionState |
Read-only session state and change events |
IAuthTokenProvider |
Bearer acquisition |
ICredentialStore |
Secure credential storage |
IAuthSession |
Sign-in and sign-out actions |
IAuthentication survives only as an [Obsolete] composite for legacy fixtures; an architecture guard forbids new production consumers. Authentication no longer depends on a REST aggregate, the cache database, or a service provider. Sign-in captures an epoch; a superseded attempt cannot publish anything.
| Test | Location (UnitTest/Architecture/) |
Enforces |
|---|---|---|
HeadConsumerRatchetTests + head-consumers.json |
same | No new head-project dependency on migrated services; scans IL and source, including service-locator calls. |
LayerDependencyRulesTests |
same | Infrastructure stays MAUI-free. |
CacheWriteBoundaryTests |
same | No raw cache write outside a generation-guarded owner. |
RemoteRawBoundaryTests |
same | Wire DTOs are mapped by raw-to-domain factories, not deserialized into domain objects. |
Each phase was its own PR into imp/blazor-hybrid-service-layer-decoupling. Detailed criteria and evidence are in SERVICE-LAYER-IMPLEMENTATION.html.
| Phase | PR | What landed | Demonstrable outcome |
|---|---|---|---|
| 0 — Safety baseline | 2505 | Cache generations, legacy-behavior fixtures, executable dependency inventory | A response released after logout cannot restore cache or Search rows |
| 1 — Transport | 2509 | Endpoint options, raw and authenticated transport, CachedRemoteQuery |
A GET recovers once from 401; a write is not replayed |
| 2 — MEC pilot | 2510 | Six MEC/LEC partitions behind IMECQueries |
Committees load live, then offline, under the same search key |
| 3 — Remaining domains | 2511 | Content, Member, Jumpseat/KCM, Notifications, Subscriptions, FlightSearch, prefetch | Per-domain fallback rules preserved |
| 4 — Documents | 2512 | Shared catalogue producer, hydration, file cache | Two callers join one fetch; cancelling one leaves the other working |
| 5 — Authentication | 2513 | Four auth ports, ordered logout participants | Logout revokes the old context before handlers can start new work |
| 6 — Transport retirement | 2514 | Menu, Favorites, FTDT, logging moved; RestService deleted |
Logging sends once; the queue owns retry |
| 7 — Facade retirement | 2515 | DataManager deleted; sign-in context reset |
Every query port resolves without constructing DataManager |
| Review remediation | 2527 (incl. 2534) | 118 review findings dispositioned; R01–R11 second pass | See resolution ledger |
Phase PR numbers come from the implementation evidence and the PR 2527 integration plan; check Azure DevOps for current merge state.
HttpClient.CachedRemoteQuery with an existing or newly registered key, a codec, and the policy that matches the legacy behavior for that domain.RawRepresentationFactory<T>; the factory stays a pure mapper.Follow MEC (MecService), the pilot: port in Application, service and API client in Infrastructure, codec beside the other codecs, DI in InfrastructureServiceCollectionExtensions. If the key should be searchable, onboard it per SEARCH-CONTENT-ONBOARDING.md.
| Rule | Why |
|---|---|
Never write the raw API cache outside a TryExecute with a pre-fetch generation |
Reopens the post-logout repopulation race; CacheWriteBoundaryTests will fail |
| Never rename an existing cache key | Cold cache and blank Search after upgrade |
| Never change a persisted JSON shape as a side effect | No cache purge is part of this work; old installs must still read their data |
| Never add automatic retry to a non-idempotent write | A lost reply can double-apply, and for JWT exchange it counts toward lockout |
Never inject IAuthentication in new code |
Use the narrow port you need |
| Never put a MAUI type in Infrastructure | LayerDependencyRulesTests; use a head adapter behind a port |
| Configuration goes through the Options pattern, not the factory | Keeps factories pure; see the project rule on RawRepresentationFactory |
DataManager, IDataManager and RestService. All three are gone at the integration tip; treat references as history.IAuthentication remains as [Obsolete], pending migration of the legacy auth test fixtures.IRestEndpointProvider may still appear as an options-backed compatibility port; do not add consumers.PR 2543 carries the whole refactor into imp/blazor-hybrid. Its own description is the source of truth for the current state; the table below reflects the validation record at code revision 342d355e:
| Item | State |
|---|---|
| Automated validation | 2,985 Debug / 2,969 Release / 4 Docs passing; iOS and Android Debug/Release builds with 0 errors |
| Device walkthroughs | iOS and Android functional and live walkthroughs completed per the PR description |
| Logged-out Home paint (was the hold) | Corrected in d30a8d90 and 342d355e: logged-out Home renders from cached or embedded styling through the existing orchestrator, then refreshes live theming. On an iPhone 17 Pro Max simulator (iOS 26.5), Home paint with a 3-second delayed site response went from 3,057 ms to 61 ms. Details and the full matrix are in the validation record. |
| Merge policy | Default merge only (no squash or rebase); one human approver; auto-complete off |
| Not part of this work | AB#2857 push tags, AB#2858 hub tags, AB#2859 theme overwrite, AB#2860 secret scan |
| Doc | Use it for |
|---|---|
| Decoupling plan | Design gates, phase acceptance criteria, risks, open questions |
| Implementation evidence | Per-phase behavior notes and criterion dispositions |
| Phase 0 inventory | Dependency ratchet, route/serializer dispositions, persisted-JSON decisions |
| Review 2026-09-21 | The 118 original findings |
| Review resolution | Disposition of each finding |
| Review validation | Exact SHAs, suite counts, native builds |
| Test migration map | Where each removed legacy test now lives |
| N-Tier architecture | Layer rules and project structure |