You can put an AI agent behind a REST endpoint in an afternoon. It will pass every demo, then quietly fall apart in production. The problem is rarely the model.
The real AI agents vs microservices gap is this: four guarantees you’ve relied on for years stop being true. Same input gives the same output. State lives outside the service. The contract is fixed. A failed call is safe to retry. The moment an LLM decides what happens next, all four can break.
This is the first post in the Designing AI-Native Applications series, and it takes the AI agents vs microservices question head-on. An agent
is not a microservice with a brain bolted on. It’s a different kind of system. By the end you’ll have a six-point mental model and a clear rule for when an agent is the wrong tool.
- An agent breaks the guarantees a service gives you: same input → same output, state kept outside, a fixed contract, and safe retries.
- The dividing line is who picks the next step at runtime — your code (service) or the model (agent). Everything else follows from that.
- Agents don’t replace services; they call them. Reach for an agent only when you can’t write the steps down in advance.
Why Your Microservice Instincts Misfire
A microservice is really just a predictable function with a network in front of it. You send a request, it runs the logic you wrote, it returns a response. State is pushed out to a database so the service stays easy to replace. That predictability is why microservices scale. You can cache them, load-balance them, retry them, and test them against a fixed expected answer. Martin Fowler’s microservices definition rests on this: small, independent services talking over simple, predictable contracts.
An agent quietly takes away the one assumption that makes all of that work.
Look at the right side of that diagram. The agent doesn’t follow a path you wrote. It runs a loop: the model picks the next step, calls a tool, reads the result, and decides again. The path is chosen while it runs, not fixed when you deploy.
That one change spreads to everything else. If the path isn’t fixed, the output isn’t repeatable. If the output isn’t repeatable, retries aren’t safe and tests can’t check for one correct answer. Your microservice instincts don’t misfire because they’re wrong. They misfire because they assume a guarantee the agent no longer gives you.
AI Agents vs Microservices: The Six Differences That Matter
Run any candidate agent
through these six questions and you’ll know which kind of system you’re really building. The first row causes all the rest.
| Dimension | Traditional service | AI agent |
|---|---|---|
| Who picks the path | Your code, at deploy time | The model, at runtime |
| Repeatability | Same input → same output | Same input → may vary |
| State | Stateless; pushed to a DB | Carries context across steps |
| Interface contract | Fixed schema, typed I/O | Fluid; intent in, action out |
| Failure & retry | Idempotent (safe to replay) | Replay may take a new path |
| Testing & cost | Check exact output; cheap, flat | Check behaviour; variable, pricier |
The trade runs straight down the table. A service gives you predictability, cheap retries, and tests that check for an exact value. An agent gives all three up to buy flexibility — the ability to handle inputs you never listed in advance. You’re not picking the smarter option. You’re choosing how much predictability to trade away.
The runtime comparison in Simple AWS’s agentic-AI series makes the same point from the ops side: the deployment looks familiar, but the runtime behaviour you have to plan for is not.
The Same Feature, Two Ways
Abstract is slippery, so take one feature — process a refund — and build it both ways. First as a traditional service, a small, testable unit:
# Service: you decide every step, in order, every time.def handle_refund(order_id: str) -> Refund:order = orders.get(order_id)assert order.eligible_for_refund() # fixed rulerefund = payments.refund(order.charge_id) # one pathreturn refund
Same input, same path, every time. You can retry it safely, cache the eligibility check, and write a test for the exact result. Now the agent version:
# Agent: the model decides the next step, in a loop.while True:reply = model(messages, tools=[get_order, check_policy, issue_refund, ask_human])if not reply.tool_calls: # model decided it's donebreakfor call in reply.tool_calls: # run whatever it chosemessages.append(run_tool(call))
Notice what you didn’t write: the order of the steps. The model might check the policy twice, skip a step, or escalate to a human. And it may not make the same choice next time, even on the same request.
I shipped a version of exactly this once. Across three runs of the same refund, it took three different routes, called a tool it didn’t need, and cost several times more than the four-line service. It was more capable and worse for the job — because the task never needed anything decided.
If the line you care about is agent versus workflow rather than agent versus service, I drew that one in detail in AI Agent vs Workflow. This post is about what changes when an agent replaces a service.
When an Agent Is the Right Call
Agents earn their unpredictability on exactly one kind of problem: a task you can’t write down in advance. Anthropic’s Building Effective Agents says it plainly — use the simplest thing that works, and give the model autonomy only when the task needs it.
Reach for an agent when:
- The path depends on what you find along the way — you can’t know the next step until the last result comes back.
- Picking the right tool is the hard part — you have many tools, and which one to use, and in what order, changes every request.
- The input is open-ended — plain-language requests, messy documents, anything a fixed schema can’t capture.
That last point is why an agent’s contract
is so different from a service’s typed endpoint. For the API-shaped version of this — when a model needs a tool protocol instead of a REST call — MCP vs REST API covers the interface side.
When NOT to Use One
Here’s the part the hype skips: for most of your system, a plain service is still the right answer, and reaching for an agent is over-engineering. Anything transactional, audited, billed, or compliance-bound needs a fixed path — because it varies by run
is simply not acceptable there.
Keep it a plain service when:
- You can write the steps down — if there’s no
…and then it depends,
you don’t need a model to decide anything. - Predictability is the point — payments, login, anything where the same input must give the same result.
- Latency and cost are tight — a service makes one call; an agent can make many, and each one is a round-trip to the model.
The healthy pattern isn’t an agent instead of services. It’s an agent that calls your services as tools. The plain services keep doing the reliable work. The agent adds a thin decision layer on top, only where the path can’t be fixed.
Quick Recap
- Service: your code picks the path. Repeatable, stateless, cheap, easy to test.
- Agent: the model picks the path as it runs. Flexible, stateful, pricier, varies by run.
- The six differences — path, repeatability, state, contract, retry, testing — all come from the first.
- Agents call services; they don’t replace them.
- Default to a service. Add an agent only when you truly can’t write the steps down first.
Conclusion
The AI agents vs microservices choice really comes down to one thing: stop treating an agent as a fancier service. Treat it as a different runtime contract — one that trades repeatable output, statelessness, and safe retries for the ability to handle what you couldn’t script. Get that framing right and most of the hard questions answer themselves. You put plain services where you need trust, and an agent only at the one decision that truly needs it.
Which of the four guarantees — same input giving the same output, statelessness, a fixed contract, or safe retries — broke on you first when you wrapped a model in an endpoint? Tell me in the comments.
Read next: Context Engineering Architecture — Part 2 of Designing AI-Native Applications, on how an agent’s context is assembled, grounded, and kept from drifting.
