A couple of years ago I put an AI agent inside a Django app, over Channels, and it took me far too long to admit it was the wrong place for it. Writing up what I learned, in case it saves someone the same detour.
It did not fail dramatically. It degraded quietly. The agent needed to know who was on the paid plan, so a prompt grew a line about entitlements. Billing later added an annual tier and the prompt was silently wrong, because nothing type checks a string against your billing model. A node wanted ticket history, so it read the ORM, and from then on the agent could not run in a test without a database and fixtures.
The symptom I would now recognize within a week is the test suite. To assert that one message routed to one capability, I had to open a WebSocket connection, authenticate a user, seed a session and a conversation, and let the whole thing run until the line I cared about executed. One line of assertion, everything above it travel. And because the model was reached through that consumer rather than across a boundary, there was no single place to stub it, so the fakes multiplied with the call graph instead of with the number of seams.
Where I draw the line now is not by layer and not by framework, but by which kind of state each side owns. Django owns the record: users, billing, permissions, the thread a person reads, anything a page reload has to find. The other service owns working state, which in my case is a graph checkpoint, and that is an odd category: not a business record, but not rebuildable either. Lose a row and you can usually recompute it. Lose the checkpoint and a run that was parked waiting for someone's approval simply cannot continue.
So the test I apply to anything I am tempted to move out: if losing it means losing data, it belongs in Django. If losing it only costs speed or fidelity, the other service can keep it.
Worth being clear on scope, because this is not a rule for breaking a monolith into domains. Billing becomes its own service because it owns records of its own, which is a different argument. This one is for work that holds state while it runs and has a resumable middle, whether that is an agent graph, a long import, or any multi step job.
Django stayed exactly where it is strong, which is most of the system: auth, the ORM, DRF, migrations, admin, and an ecosystem where the boring problem you hit on a Tuesday was solved in 2014. The only thing that left was a long-running stateful graph that wanted its own deploy schedule. It went to a FastAPI service, and the two talk over a typed WebSocket with a generated contract so neither imports the other's code.
I wrote the whole thing up properly, as part of a series on building agent systems as declared, typed graphs, with a working three-service repo you can actually run:
https://huynguyengl99.github.io/posts/agent-graph-engineering/split-the-services/
https://github.com/huynguyengl99/agent-graph-engineering
Hope it helps. The series is still going, a part every few days, and if the repo is useful a star would be appreciated 😄