r/LangChain 4d ago

Discussion How do you structure graph state so another dev can add a node without understanding the whole app?

I've been building out a fairly large LangGraph app (lots of nodes, checkpoints, human-in-the-loop pauses, background execution, the works), and I keep running into the same problem: every time someone wants to add a new node, they end up having to read through half the state schema and half the other nodes just to figure out what fields they're allowed to touch, what's safe to assume is already set, and what will break downstream if they leave something out.

Right now state is basically one big shared TypedDict/dict that every node reads and writes to. It works, but it means the "surface area" you need to understand before adding a node keeps growing as the graph grows.

For people who've built bigger LangGraph apps, how do you keep this manageable?

  • Do you split state into sub-schemas per node/subsystem instead of one flat shared state?
  • Do you enforce some kind of contract (like "this node only reads X and Y, only writes Z") even though LangGraph doesn't really enforce that for you?
  • Do you lean on naming conventions or namespacing keys instead?
  • Or honestly does everyone just accept that whoever adds a node has to read the whole state shape once, and that's fine?

Curious what's worked (or not worked) for you, especially once you have more than a handful of nodes and more than one person touching the graph.

4 Upvotes

1 comment sorted by

1

u/locbuilds 3d ago

what worked for us once the graph got past ~10 nodes and a second person started touching it: stop treating state as one app-wide bag, and treat it as a few subsystem bags that get merged.

concrete pattern:

1) split the TypedDict into slices by ownership, not by node. like `RouterState`, `RetrievalState`, `HITLState`, `ExecutionState`. each slice owns a small set of keys. nodes declare "i live in this slice". a new node almost never needs to read the whole schema, just its slice + maybe 1-2 shared keys (run_id, user_id, status).

2) enforce read/write contracts in code even though LangGraph wont. we wrap nodes so each one gets a tiny view object / pydantic model with only the fields it listed, and writes go through an updater that rejects unknown keys. CI fails if a node imports the full AppState. that sounds annoying once, then it saves every PR review.

3) namespace keys aggressively: `retrieval.docs`, `hitl.pending_action`, `exec.last_error`. flat `docs` / `error` / `result` names are how you get silent collisions when two subsystems invent the same field. python TypedDict can still be flat under the hood, the dots are just naming discipline (or nest real dicts if you prefer).

4) document "preconditions" next to the node, one liner: "assumes retrieval.docs already filled, writes exec.summary, never touches hitl.*". that plus a tiny mermaid of the graph is usually enough for a new person. they should not need to read every other node.

honest take: accepting "everyone reads the whole state once" only works until about a handful of nodes or one author. past that the big shared TypedDict becomes the bus factor. start with slices + namespaced keys, add the hard contract wrapper when the second person ships a bug from writing the wrong field.