The header shows 3 items while the cart page shows 0
Angular incident
Cart badge and cart page drift out of sync
The header badge and the cart page stop agreeing because one feature quietly creates a second CartService instance.
Failure signals
The clues you start with
Refreshing the cart page briefly 'fixes' only one side
The feature component has its own CartService provider
Overview
What failed in production?
Start by making the bug legible: what the user saw, why it mattered, and what the product was doing underneath.
Symptom
A user adds products from the catalog and sees the header badge move to 3, but when they open the cart page it says the cart is empty. Sometimes the cart page then adds an item and the badge still stays at 3.
User impact
Users stop trusting checkout because the app cannot agree on what is actually in the cart.
Environment
Angular storefront. `CartService` uses `providedIn: 'root'`, but the cart feature shell also lists `providers: [CartService]`. The header badge and the cart page both inject the service.
Evidence pack
What you can inspect before answering
Visible mismatch
The header and the cart page can both be open at the same time, and they show different item counts even though they are supposed to share one source of truth.
Runtime clue
A debug log prints two different `CartService` instance ids: one in the app shell and one inside the cart feature.
Cart feature shell
@Injectable({ providedIn: 'root' })
export class CartService {
items: CartItem[] = [];
}
@Component({
selector: 'app-cart-shell',
templateUrl: './cart-shell.component.html',
providers: [CartService]
})
export class CartShellComponent {
constructor(public cart: CartService) {}
}Stage 1: likely root cause
Why are the badge and cart page out of sync?
Pick the explanation that fits Angular DI, not just generic state bugs.
A race can cause flicker, but the stronger clue here is that the two views are reading from different service instances.
Correct. Angular DI is hierarchical, and the component-level provider shadows the root service for that subtree.
Change detection can hide symptoms, but it does not explain two different service instance ids.
That is the trap. Re-providing the same service in a component or route can still create another instance.
Stage 2: debug priority order
What would you check first? Put the best three at the top.
First prove whether the app has one shared cart service or more than one.
- #1Log the CartService instance identity in the header and in the cart feature.This proves whether the two screens share the same service.
- #2Inspect route, feature, and component providers to see where CartService is re-provided.You need to know which injector branch owns the duplicate instance.
- #3Reproduce the mismatch by adding an item in one screen and reading the count in the other without a full reload.Confirm it is really a shared-state split, not a one-off timing issue.
- #4Replace the whole cart flow with NgRx before confirming the service scope problem.Too large and too early.
- #5Scatter `detectChanges()` calls until both screens appear to match.Masks the symptom without proving the root cause.
Only the top 3 positions are scored.
Expected: Log the CartService instance identity in the header and in the cart feature.
Expected: Inspect route, feature, and component providers to see where CartService is re-provided.
Expected: Reproduce the mismatch by adding an item in one screen and reading the count in the other without a full reload.
Stage 3: best fix set
Which changes belong in the actual fix?
Pick the fixes that restore one clear source of truth.
Correct. If the cart should be shared app-wide, the duplicate provider has to go.
That bakes the bug into the architecture instead of fixing the scope mistake.
Correct. Explicit scope is fine; accidental shadowing is not.
Correct. This helps prevent the same DI bug from coming back in another feature.
That hides the mismatch but leaves the duplicate instance problem in place.
Stage 4: regression guard
What is the best guardrail for this bug?
Pick the check that would catch a split service instance early.
Correct. This checks the user-facing contract that both screens must share the same cart state.
Performance metrics do not guard service scope or shared-state correctness.
Manual smoke tests help, but they are too weak as the main guardrail.
Feature-scoped providers are sometimes correct. The guardrail should check intent, not ban a useful tool.
You noticed the mismatch, but not the injector boundary
The cart bug looked like shared state, but the real problem was still service scope.
You were close, but one fix still treated the symptom
You connected the issue to DI, but part of the answer still leaned on refreshes or other workarounds.
You diagnosed the duplicate service instance clearly
You proved the service split, fixed the provider scope, and protected the shared-state contract.
Ideal runbook
How a strong answer should flow
- 1Prove whether both screens use the same CartService instance.
- 2Inspect where CartService is provided and which injector branch shadows the root service.
- 3Remove the accidental duplicate provider or scope it intentionally with a different contract.
- 4Protect the fix with a cross-screen shared-state test.
Reflection
Reflection note
When would a component-level provider actually be the right choice instead of a bug?
Angular shortcut
When two parts of an Angular app disagree on shared state, ask early whether they are really using the same service instance.
Strong answer signals
- You mention hierarchical DI, not just generic state bugs.
- You prove the duplicate instance before changing architecture.
- You restore one clear source of truth instead of syncing two copies.