Course Content
Scenario-Based AI Engineering Questions
26 sections · 146 lessons
Scenario – 3: Shared State Collision
What you need to know
The scenario: several branches run in parallel and write to the same state field. Results go missing, or the run fails with a concurrent-update error.
Supersteps and channels
LangGraph runs in supersteps. All nodes that are ready run together, then all their updates are applied to the state at once. Each key in the state is a channel. By default a channel holds one value, so two writes to it in one superstep are a conflict.
Option 1: reducers
1import operator2from typing import Annotated, TypedDict3from langgraph.graph.message import add_messages45class State(TypedDict):6 query: str7 findings: Annotated[list[str], operator.add] # branches' lists are concatenated8 messages: Annotated[list, add_messages] # merges messages, replaces by idNow every branch can return {"findings": [...]} and nothing is lost.
Option 2: namespaced keys plus a join node
1class State(TypedDict):2 query: str3 web_result: str | None4 db_result: str | None5 docs_result: str | None6 merged: str | None78def join(state: State):9 parts = [state["docs_result"], state["db_result"], state["web_result"]] # internal docs first10 return {"merged": "\n\n".join(p for p in parts if p)}Reducer on a shared key
- Very little code
- Order of results is not guaranteed
- Cannot express "internal docs beat the web"
- Lists grow with every loop
Separate keys plus a join node
- One key per branch, no conflicts
- Merge logic is plain, testable code
- Can apply precedence and deduplication
- Slightly more schema to write
For a dynamic number of branches (one per sub-question), use Send, which gives each branch its own input, with a list reducer to collect results.
Two rules to state in an interview
- Order independence. Branches finish in any order, so a reducer must give the same result whichever update arrives first.
- Unbounded growth. Additive reducers keep appending. In a long-running or looping graph, trim or summarise in the join node, or the context quietly grows until it is too big.
A real-life example
Scenario, numbers made up. A due-diligence graph runs three branches — company filings, news and internal CRM notes — and each writes summary. About one run in five fails with a concurrent-update error, and in an older sequential version, CRM notes sometimes overwrote the filings summary.
The team switches to filings_summary, news_summary and crm_summary, plus a join node that orders them, deduplicates repeated facts and flags conflicts ("news says the CFO resigned; filings list her as CFO"). They add a test that runs the branches in both orders and asserts the merged state is identical. The errors stop, and analysts get a merged view that states conflicts instead of hiding them.
Follow-up questions to expect
- "Why not use a lock?" — Updates are applied by the runtime between supersteps, not by your code, so there is nothing to lock. The fix is declaring how updates combine.
- "What does
add_messagesdo differently fromoperator.add?" — It merges by message ID, so a message with an existing ID replaces the old one instead of duplicating it. - "When is a reducer the right choice?" — When results are the same type, order does not matter, and simple collection is all you need.