Scenario-Based AI Engineering Questions

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

Python
import operatorfrom typing import Annotated, TypedDictfrom langgraph.graph.message import add_messagesclass State(TypedDict):    query: str    findings: Annotated[list[str], operator.add]    # branches' lists are concatenated    messages: Annotated[list, add_messages]         # merges messages, replaces by id

Now every branch can return {"findings": [...]} and nothing is lost.

Option 2: namespaced keys plus a join node

Python
class State(TypedDict):    query: str    web_result: str | None    db_result: str | None    docs_result: str | None    merged: str | Nonedef join(state: State):    parts = [state["docs_result"], state["db_result"], state["web_result"]]   # internal docs first    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_messages do differently from operator.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.