“Let’s split it into agents” has become the AI equivalent of “let’s make it a microservice.”
Sometimes the boundary is useful. Sometimes it only creates more state, more coordination, and a harder failure to explain.
In production workflows I have built and reviewed, the most dangerous assumption is that separate agents should run in parallel. Parallelism is safe only when the branches are genuinely independent. If one branch changes the world while another is evaluating it, both agents can make locally reasonable decisions that are unsafe together.
Disclosure: I maintain AgentInspect, an open-source execution-evidence tool mentioned once later in this article. The ADK architecture and examples here stand independently of that project.
Google ADK 2.0 makes workflow topology explicit through graph-based Workflow objects. That is valuable because sequences, branches, and joins become part of the program instead of an agreement hidden in a supervisor prompt.
Imagine a system preparing a hotel recommendation.
It needs live inventory, company travel policy, and a final recommendation. Inventory lookup and policy evaluation can run concurrently because both observe the same request and neither changes shared state. The final decision must wait for both.
Now consider a different pair of operations:
Those branches are not independent. Running them concurrently can make the upgrade decision depend on state that no longer exists.
Before drawing a parallel branch, ask:
If those answers are unclear, parallel is an optimization you have not earned yet.
ADK’s TypeScript Workflow graph can express two independent branches and a join barrier directly:
import { JoinNode, node, NodeContext, Workflow } from "@google/adk";
type Request = {
city: string;
maxNightlyPriceUsd: number;
};
const fetchInventory = node(
async (_ctx: NodeContext, request: Request) =>
searchHotels(request.city, request.maxNightlyPriceUsd),
{ name: "fetch_inventory" },
);
const evaluatePolicy = node(
async (_ctx: NodeContext, request: Request) =>
checkTravelPolicy(request),
{ name: "evaluate_policy" },
);
const evidenceReady = new JoinNode({ name: "evidence_ready" });
const chooseRecommendation = node(
(_ctx: NodeContext, results: Record<string, unknown>) => {
if (!("fetch_inventory" in results) || !("evaluate_policy" in results)) {
throw new Error("Recommendation requires both evidence branches");
}
return selectCompliantHotel(
results["fetch_inventory"],
results["evaluate_policy"],
);
},
{ name: "choose_recommendation" },
);
export const rootAgent = new Workflow({
name: "hotel_recommendation_workflow",
edges: [
["START", fetchInventory, evidenceReady],
["START", evaluatePolicy, evidenceReady],
[evidenceReady, chooseRecommendation],
],
});
The topology is the contract:
START ─┬─> fetch_inventory ──┐
└─> evaluate_policy ──┴─> evidence_ready ─> choose_recommendation
JoinNode waits for every predecessor and passes the next node a record keyed by predecessor name. Each predecessor must produce output. Validate that record at the join instead of allowing a missing branch to surface as an unrelated failure several nodes later.
Some work is naturally sequential:
normalize request
↓
search inventory
↓
request approval
↓
create booking
↓
send confirmation
The matching ADK graph is deliberately boring:
export const bookingWorkflow = new Workflow({
name: "booking_workflow",
edges: [[
"START",
normalizeRequest,
searchInventory,
requestApproval,
createBooking,
sendConfirmation,
]],
});
That sequence is safer than asking a supervisor model to remember the required order on every run. The model can still make bounded decisions inside individual nodes; the workflow owns the invariant.
This is a recurring production pattern:
Use probabilistic reasoning inside deterministic control flow.
Shared mutable state is where fan-out becomes dangerous.
If two branches write selected_hotel, the last writer wins. If one updates a booking while another reads it, behavior depends on timing. If both send a notification, the user receives duplicate side effects.
A safer ownership rule is:
Stage: Parallel branches
Responsibility: Gather and normalize evidence
Stage: Join
Responsibility: Verify all required evidence arrived
Stage: Decision node
Responsibility: Select one outcome
Stage: Mutation node
Responsibility: Own the state transition or side effect
When distributed mutation is unavoidable, use an idempotency key, resource-level concurrency control, and a durable result record. Do not rely on a model to notice that another branch is already acting.
Parallelism introduces failure combinations that a happy-path diagram hides:
Each branch needs a timeout and retry budget appropriate to its operation. The join needs a rule for partial failure: fail closed, use an explicitly degraded mode, or ask for human review. “Continue with whatever arrived” should be a named policy—not an accident.
ADK can expose and consume remote agents through the Agent2Agent protocol. That is useful when a capability belongs to another team, runtime, deployment, or trust domain.
But a remote agent is not a helper function. It introduces:
Keep tightly coupled work in one local workflow unless there is a real service boundary. Use A2A when that boundary already exists for organizational or platform reasons, not because a diagram looks more “agentic.”
Output-only testing misses topology regressions.
A stable execution contract for the recommendation workflow might require:
fetch_inventory and evaluate_policy both complete;choose_recommendation occurs after evidence_ready;The evidence can be rendered as an execution tree:
hotel_recommendation_workflow
├─ fetch_inventory
├─ evaluate_policy
├─ evidence_ready
└─ choose_recommendation
A local evidence tool such as AgentInspect can check required, forbidden, and ordered operations after those ADK events have been mapped into its run format. That wording is deliberate: AgentInspect does not currently advertise a first-class ADK adapter, so the integration boundary should remain explicit until one ships.
The goal is not to make the model deterministic. It is to make the workflow contract deterministic.
Multi-agent architecture is not automatically better than one well-designed agent. Parallel execution is not automatically faster once retries, joins, and coordination are included. Remote delegation is not automatically modular once network contracts are involved.
Use parallel branches for independent evidence gathering. Use sequences for dependent work. Give one node ownership of each mutation. Use remote agents only at genuine service boundaries. Record the execution shape so the team can see what actually happened.
The production measure is not how many agents participated.
It is whether the system reached the right outcome without conflicting actions, hidden races, or an execution path nobody can explain.