abelcastro.dev

Monorepos, Microservices, and the Architecture Pendulum

2026-04-03

spec-driven-developmentmonorepomicroservices

How I Use AI to Write Blog Posts

2026-04-02

mcpdjangoclaude

Django-style test DBs with Prisma: what I built in my monorepo (PoC)

2026-01-08

typescripttestingprisma
1
2
3
...
1011

Abel Castro 2026 - checkout the source code of this page on GitHub - Privacy Policy

Around 2014-2017, microservices were the only architecture anyone wanted to talk about. Netflix, Spotify, and Amazon were the poster children, and the message the industry absorbed was clear: monoliths are legacy, microservices are modern. Conference talks, blog posts, and hiring trends all reinforced that framing. If you were building a monolith, you were doing it wrong.

Then reality caught up.

The pendulum swings back

Teams discovered the hidden costs of distributed systems. Distributed tracing became a nightmare, network latency between services added up, data consistency across service boundaries was painful, and the operational overhead of managing dozens of repos with independent CI pipelines burned through engineering time. People like Kelsey Hightower and even Sam Newman (who wrote the book on microservices) started cautioning against premature decomposition. Martin Fowler's team put it simply: don't start with microservices, earn them.

The pattern became common enough to get its own narrative: monolith to microservices and back to monolith. In 2023, Amazon Prime Video published a case study where they moved back from microservices to a monolithic architecture for a video monitoring workload and cut costs by 90%. That made waves precisely because it came from Amazon itself.

The industry didn't swing all the way back though. The current consensus is more nuanced: start with a well-structured modular monolith, extract services only when you have a clear operational or scaling reason, and keep your architecture decisions tied to your actual problems rather than to conference hype.

Monolith, microservices, monorepo: clearing up the terms

One source of confusion is the word "monorepo" getting treated as a synonym for monolith. They are fundamentally different concepts.

A monolith is an architectural and deployment pattern. One deployable unit, one runtime, tightly coupled code.

A monorepo is a source code management strategy. One repository containing multiple projects, libraries, or even independent services.

These are orthogonal decisions. You can mix and match them freely, and all four combinations are valid: one repo with one deployable unit is the classic monolith setup most small teams start with. One repo with many deployable services is the Google-style monorepo with microservices. Many repos with many services is the "pure" microservices approach that was popular around 2015. And many repos with a monolithic deployment is possible too, though it is rare and usually painful.

The confusion happens because "mono-" appears in both words and because the back-to-simplicity energy applies to both dimensions at the same time. Teams burned by 50 repos with 50 CI pipelines are consolidating into monorepos while teams burned by distributed complexity are consolidating toward monolithic architectures. The motivations overlap (reducing accidental complexity) but the decisions are independent.

Why monorepos are gaining ground in AI-first development

Here is where things get interesting. AI coding agents are fundamentally context-dependent, and monorepos maximize the context surface available without friction.

When an agent like Claude Code needs to implement a feature that touches both your API and your frontend, having both in the same repo means it can trace the contract end-to-end: the API endpoint definition, the shared types, the frontend call, the error handling. It does not need you to manually explain what the other repo looks like.

A few specific areas where this matters:

Type and contract coherence. In a monorepo with shared types (a /packages/shared folder, for example), the agent can see that changing an API response shape requires updating the frontend consumer. Across repos, that connection is invisible unless you explicitly describe it.

Refactoring scope. Renaming a field, deprecating an endpoint, or changing an auth flow are cross-cutting concerns. An agent in a monorepo can search, understand, and modify everything atomically. Across repos it becomes two separate sessions with you acting as the bridge.

Testing context. Integration tests that verify frontend-backend interaction live naturally in a monorepo. The agent can run them, see what breaks, and fix both sides in one pass.

The honest downsides

Monorepos are not free. CI complexity scales with repo size. A change in a shared utility triggers builds and tests across every project in the repo. Google solved this with Bazel and massive infrastructure investment, but most teams are not Google. The tooling tax (Nx, Turborepo, build caching, affected-project detection) is real and adds its own layer of complexity. AI agents do not make your CI pipeline faster.

Context windows are finite too. A massive monorepo with 500k lines is going to hit token limits regardless of how well-structured it is. What matters more than repo structure is organization within the repo: clear module boundaries, good naming, and well-organized directories. A chaotic monorepo can actually be worse for an agent than two clean, well-documented repos with a shared API spec.

Where separate repos still win

Conway's Law has not been repealed. Architecture follows organizational structure, not tooling. If two teams own two services with different deployment lifecycles, forcing them into one repo for AI convenience creates human coordination problems. AI optimizes the coding phase, but software delivery is still a people problem.

Cross-repo tooling is catching up too. MCP servers can index your API schema, your frontend types, and your deployment config across repos and give an agent functionally equivalent context to a monorepo. In some cases the context is arguably better because it is curated rather than "here is everything, figure it out." Think of it like a database index versus a full table scan.

The likely future is that repo structure becomes less important as tooling abstracts it away. The agent of 2028 probably will not care whether your code lives in one repo or five. It will have indexed access to all of it, understand the dependency graph, and operate across boundaries seamlessly. The monorepo advantage we feel today is real, but it might be a symptom of tooling immaturity rather than a fundamental architectural truth.

One repo for code, rules, and agent knowledge

There is another monorepo win that gets less attention: the agent's own configuration lives alongside the code it operates on.

In an AI-first workflow, your project carries more than just source code. It has agent rules (CLAUDE.md, .cursorrules, AGENTS.md) that define project conventions and constraints. It has skills that encode task-specific playbooks, like how to build a chart component or how to write a migration in your project. It has architecture decision records that capture why the project uses certain libraries or patterns. And it has the codebase itself, which serves as a living reference for how things are actually built.

In a monorepo, all of this is available to the agent in a single context. The agent reads the rules, understands the conventions, looks at existing components for reference patterns, and implements the new feature following the established approach. There is no gap between "what the agent knows about the project" and "what the project actually looks like." The rules file says "use MUI X Charts Pro for all chart components," and three directories over the agent can see exactly how the team has used that library before.

In a multi-repo setup, this knowledge gets fragmented. Each repo might have its own rules file, but the project-wide conventions live... where? A shared wiki? A Confluence page the agent cannot read? The senior developer's head? The monorepo keeps the entire knowledge surface in one place: code, conventions, patterns, and agent configuration. A new agent session (or a new team member) can orient themselves from a single starting point.

This matters more than it sounds. The quality of an agent's output is directly proportional to the quality of the context it has. A well-maintained CLAUDE.md in a monorepo with clear conventions and reference implementations will produce better results than a detailed prompt in a repo where the agent has no project knowledge to draw from.

Bonus: spec-driven development and monorepos

If you are using spec-driven development, having your specs live in the same repo as the implementation is another natural win. The agent can validate conformance in real time, see drift between the spec and the code, and keep both in sync without you acting as the bridge.

That said, there is a growing conversation about whether the current wave of SDD tooling is worth the overhead it introduces. Tools like Kiro, spec-kit, and OpenSpec all propose structured spec workflows, but some practitioners are finding that the maintenance burden of all those markdown files creates its own problems. I have been exploring this topic and I am not entirely convinced the hype around SDD will age well. It reminds me of the early microservices enthusiasm: a good idea applied universally without enough regard for when it actually helps and when it just adds overhead. I will dig into that in a future post.

I like writing about software development but I rarely do it. The problem was never a lack of ideas. I have plenty of opinions about testing, framework choices, and how to build things. The problem was the time it took to go from "I want to write about X" to a published post. Sitting down, drafting, editing, formatting, and finally getting the thing into my blog's CMS felt like a side project on top of my actual side projects.

So I decided to treat it like any other engineering problem: find where the friction is and remove it.

I needed 3 things to make this work: a way to create posts programmatically in my blog, a bridge between Claude and that API, and a way to teach Claude how I write and what I care about.

1st: A POST endpoint for my blog

My blog runs on Django. Until recently, the only way to create a post was through the Django admin. That's fine for manual writing, but it doesn't connect to anything else.

I added a simple API endpoint that accepts a POST request with the fields I need for a blog post: title, slug, meta description, content in Markdown, and tags. The post gets created as a draft so nothing goes live without my review.

You can see the pull request here. It's nothing fancy. A straightforward DRF endpoint with token authentication. The kind of thing Django makes easy and boring in the best way.

2nd: An MCP server to connect Claude to my API

MCP (Model Context Protocol) is a standard that lets AI assistants like Claude interact with external tools and services. Instead of copying text from a chat window and pasting it into a form, Claude can call my blog's API directly.

I built a small MCP server that exposes my blog's POST endpoint as a tool Claude can use. The server runs locally and the configuration looks like this:

{
  "mcpServers": {
    "blog": {
      "command": "uv",
      "args": ["run", "server.py"],
      "cwd": "/absolute/path/to/blog-mcp"
    }
  }
}

With this in place, I can ask Claude to create a draft and it sends the post data directly to my Django backend. No copy-pasting, no context switching.

3rd: A Skill that knows how I write

This was the most interesting part. Claude is good at generating text, but generic AI-written content reads like generic AI-written content. I wanted posts that reflect my perspective, my background, and my opinions.

Claude Skills are instruction files that teach Claude specific workflows and preferences. I created one that captures how I think about software development: my experience with Django and TypeScript, my appreciation for simplicity over complexity, my focus on testability, and the way I prefer to explain things without hiding behind buzzwords.

The Skill also defines the output format so every post comes out with the exact fields my API expects. No reformatting needed.

The workflow now

Writing a post now looks like this:

I start with an idea. Maybe something I ran into at work, a pattern I found useful, or a topic I have a strong opinion about. I open Claude and describe what I want to write about, the key points I want to cover, and any specific angle I want to take.

Claude uses the Skill to generate a complete draft that matches my style and structure preferences. I read through it, adjust whatever needs adjusting, and when I'm happy with it I ask Claude to push the draft to my blog via the MCP server.

The post lands in my Django admin as a draft. I do a final review there, maybe tweak a sentence or two, and hit publish.

The whole process takes a fraction of what it used to take. More importantly, the friction that stopped me from writing is gone. I no longer need a free afternoon to produce a post. I need ten minutes and a clear idea.

A note about AI-generated content

Most of the words in my posts are generated by Claude. I want to be transparent about that. But here's what matters to me: every post starts with my idea, my perspective, and my direction. The Skill I built encodes my opinions and my way of explaining things. I review and edit everything before it goes live.

The goal was never to remove myself from the process. It was to remove the parts that slowed me down so I could focus on the part I actually enjoy: figuring out what's worth saying.

TL;DR

I wanted the same thing again: keep unit tests fast, but enable a subset of tests to run against a real Postgres database with migrations applied automatically similar to Django's default experience. In my monorepo PoC, I wired Prisma to support that workflow.

Note: This post describes a proof of concept. The core idea works, but I'll be explicit about what's still missing for a production-grade testing setup.

Django's test experience is integrated: test runner + ORM + migrations + DB lifecycle all come together. In the TS ecosystem, Prisma is “just” the ORM layer; you still need to decide:

  • How to initiate the DB
  • When migrations run
  • How to reset data
  • How to prevent parallel tests from stepping on each other

So I built a pragmatic approach inside my monorepo PoC.

Repo: abel-castro/blog-monorepo (NestJS API + frontend in one workspace). 

The approach (conceptually)

My Prisma integration-testing recipe is:

  1. Provide a real Postgres for tests - easiest path: docker-compose (local + CI)
  2. Use a dedicated DATABASE_URL for integration tests
    • never point integration tests at dev/prod DBs
    • ideally a separate DB name or schema
  3. Ensure schema is up-to-date before tests
    • run migrations programmatically, or via a pre-test command
    • be consistent: either “migrate” (schema history) or “db push” (fast, but less realistic)
  4. Reset data deterministically

Why Prisma feels different than Django here

Prisma is great, but it's intentionally not a full “framework”.

  • Prisma doesn't know your test runner (Jest/Vitest/node:test).
  • Prisma doesn't own your DI container (NestJS does).
  • Prisma migrations are a separate workflow (CLI / migration engine).
  • There's no universally “right” cleanup strategy.

So: you build a thin layer that standardizes your project's conventions.

What my PoC focuses on

This PoC focuses on the “Django feeling” of:

  • “I can write tests that hit a real DB”
  • “DB schema is ready when tests run”
  • “Unit tests remain fast and simple”

In other words: integration tests become easy to add.

Good points of this direction

  • More realistic tests for anything query-heavy:
  • filters, ordering, pagination
  • relations, constraints, unique indexes
  • migrations-related behavior
  • Less mocking around the ORM boundary
  • Higher confidence refactors (especially around query logic)

Current trade-offs

Even if the PoC works, there are predictable pain points:

  1. Speed
    • DB startup + migrations cost time.
    • Resetting DB between tests can dominate runtime if done naively.
  2. Isolation
    • The best isolation approaches add complexity (transactions, schemas, per-worker DBs).
    • The easiest approaches (shared DB, truncate) can allow ordering-dependent tests if you're sloppy.
  3. Parallel tests
    • Test workers + one DB can cause flakiness unless you isolate per worker.
  4. Developer friction
    • “Run DB before integration tests” has to be one command, or people won't use it.

What I'd implement next (turning PoC into a solid template)

If you want this to feel as boring as Django:

  1. A single command Something like:

    • pnpm test (unit only, no DB)
    • pnpm test:e2e (starts DB, migrates, runs integration specs, tears down)
  2. A robust reset strategy

  3. Per-worker DB isolation (parallel-safe)

    • create DB/schema names based on worker id
    • avoid cross-test collisions
  4. CI hardening

    • healthchecks
    • deterministic order
    • run integration tests in a separate job/stage

Closing

Django sets an expectation: “integration tests with a real DB should be trivial.” In Prisma + NestJS, that experience is absolutely achievable but you have to intentionally assemble the pieces.

Repo PoC: abel-castro/blog-monorepo.