DEV Community

Cover image for Most of Your Microservice Is Plumbing. What If You Stopped Writing It?
Matías Denda
Matías Denda

Posted on

Most of Your Microservice Is Plumbing. What If You Stopped Writing It?

TL;DR — Every microservice is mostly plumbing: an HTTP server, connection pools, marshalling, retries, health checks, wiring — the same in every service, rewritten with different nouns. Mycel is a declarative runtime that lets you declare that plumbing instead of writing it, then runs the result as a real production microservice. When I showed it off, almost everyone assumed "declarative = prototyping toy." This post is why that's backwards — and what the hard parts (auth, retries, migrations) look like when they're config instead of code.

Be honest about your last microservice

Think about the last service you wrote. A router. A handler. A DTO struct. A validation layer. A connection pool. A query. Marshalling the result back to JSON. Retry logic around the flaky call. A health endpoint that returns 200 whether or not the database is actually reachable. Then the next service, where you wrote the same dozen things again with different nouns.

Almost none of that is your service. It's plumbing — identical in shape across every microservice you'll ever write, just with the nouns swapped. The part that's genuinely yours — the handful of decisions that make this service this one and not some other — is a thin layer riding on a thick stack of boilerplate you've written a hundred times.

So a while ago I built a runtime where you declare the plumbing instead of writing it: you describe what connects to what, and it runs as a real microservice. I showed the three-file version in a previous post — and the response taught me something I didn't expect.

Everyone filed it under "prototyping" — and that's on me

The post got a generous, sharp response. And almost every serious reply said some version of the same thing:

  • "Love the concept for rapid prototyping and internal tools."
  • "zero-code-to-running-microservice is the fun demo… the honest test is what happens at file #4."
  • "excellent for validating concepts quickly… how does it handle complex logic as it scales beyond three files?"

Read those again and notice the vocabulary: prototyping, internal tools, the fun demo, validating concepts quickly. That's not three people describing my tool. That's three people describing a category — and concluding, reasonably, that production is where the category gives out.

I stared at this for a while not understanding why everyone landed on "toy." Then it clicked: they didn't land there. I put them there.

A reader forms a category in about five seconds, from the headline, before evaluating anything. My headline was "3 files, zero code." In the mental map of basically every engineer, "no code + something running instantly" is a settled drawer: no-code / low-code — Bubble, Zapier, Retool. And that drawer ships with a verdict already inside it: brilliant for prototypes and internal tools, quietly falls apart when you need a real production service.

So when I handed people "zero code," they didn't run the tool through their judgment and conclude "prototype-only." They dropped it in the drawer and the drawer answered for them. You can see it in the exact words they used — they were describing the shelf, not the software. And you can't argue someone out of a conclusion they imported wholesale with the label.

That's on me. "Zero code" is a true sentence that points at the wrong category. So let me throw the label out and say what the thing actually is.

What Mycel actually is

Mycel is a declarative microservice runtime. You describe what your service connects to and how data moves between those connections; the runtime interprets that config at runtime and runs as a real microservice.

Three things that drawer gets wrong about it:

It's not a visual builder. There are no boxes to click. It's config — text, in version control, reviewed in pull requests, diffable like any other code artifact. Closer in spirit to a Terraform config or an nginx.conf than to a drag-and-drop canvas. (That's the only place I'll lean on that comparison — not as a pitch, just to move you out of the wrong drawer.)

It's not a code generator. This is the one I most want to kill. One commenter put the fear precisely: "'zero code' quietly becomes 'a lot of code I didn't write and don't understand.'" That's a real and rational fear — of codegen. Scaffolding tools spray code you now own and can't fully read. Mycel generates nothing. There is no emitted Go sitting in your repo. The runtime reads your config and does the thing; "file #4" is more configuration, never a heap of opaque generated source. There's nothing to inherit because nothing was generated.

It's production-first, not demo-first. I don't run it to mock things up. I run it in production today — consuming from a hosted message broker, on Kubernetes, doing real work. The three-file version is the smallest legible example, not the ceiling.

The "file #4" test — where the engineering actually lives

The fairest pushback I got named the exact things that separate a demo from a service: validation beyond type-checking, retry logic with specific backoff curves, health checks that test downstream dependencies, env management across services — plus auth, migrations, rate limiting.

Here's the whole bet: those are configuration concerns, not code concerns. They stay declarative instead of becoming the 2,000 lines of glue you hand-write and maintain. Quick tour.

Validation beyond types — field constraints plus custom rules (regex, CEL, or WASM):

type "signup" {
  email    = string({ format = "email" })
  age      = number({ min = 18, max = 120 })
  password = string({ min_length = 12, pattern = "..." })
  plan     = string({ enum = ["free", "pro"] })
  tax_id   = string({ validator = "valid_vat" })   # custom CEL/WASM rule
}
Enter fullscreen mode Exit fullscreen mode

Retry with real backoff curves — not just "try N times":

error_handling {
  retry {
    attempts  = 5
    delay     = "1s"
    max_delay = "30s"
    backoff   = "exponential"   # or linear / constant
  }
}
Enter fullscreen mode Exit fullscreen mode

…plus a circuit breaker and per-error-class dispositions (ack / retry / requeue / reject).

Health that actually probes dependencies/health/ready doesn't just return 200. It pings every connector (DB PingContext, Redis PING, and so on) and reports per-component status; mycel check runs the same probes before the service accepts traffic.

Env across services — an env() function, per-environment overlays (environments/prod.mycel), .env support, environment-aware defaults (debug logs + hot reload in dev; JSON logs + locked-down errors in prod), and connector profiles to swap backends per environment.

And the one that most clearly isn't a toy: a transactional, multi-statement write — clear previous rows, insert a parent, capture its autoincrement id, loop over N children that reference it, all atomic in one DB transaction — declared in config. That's the kind of "complex" that used to force you straight back into code.

Want file #4 for real instead of my word for it? The auth example in the repo is a single config with persistence, JWT, MFA, brute-force lockout, sessions, and audit. That's "what happens when you add the hard stuff," and it's still config.

The honest tradeoff

I'm not going to pretend complexity evaporates. It doesn't. Auth is inherently complex, so an auth config is genuinely more involved than a three-line flow.

But be precise about what the tradeoff is. It is not "clean demo → hidden code you didn't write." It's "clean demo → more config." What you trade is writing and maintaining the how for learning the vocabulary of the what. Your nginx.conf grows as you add TLS, caching, and rate limiting — but it never turns into C you have to maintain. Same shape here.

That's a real cost — there's a vocabulary to learn — but it's a fundamentally different cost than "a thousand lines of generated Go I'm now on the hook for."

When you outgrow the vocabulary

There's a ceiling, and I'd rather name it than pretend it away. When something genuinely doesn't fit declaratively, the escape hatch is a WASM plugin: you write that one piece in Rust or Go, compile it, and the runtime calls it. That is code you write and own.

I want that seam to be obvious, not hidden. The declarative layer handles the 90% that's plumbing; the escape hatch covers the 10% that's truly yours — and you never lose the ability to drop to real code for it. A tool that pretends it covers 100% of cases is lying; one with a clear seam is just being honest about where the line is.

Prototyping vs. production is the wrong axis

Here's the reframe I should have led with.

"Good for prototypes, bad for production" treats maturity as the axis. It isn't. The real axis is plumbing vs. logic that's genuinely yours. Every microservice is mostly plumbing — HTTP server, connection pools, marshalling, retries, health, wiring — with a handful of decisions on top that make it this service and not some other one.

Mycel removes the plumbing. That's just as true when you're prototyping as when you're on Kubernetes serving real traffic — it's the same tool doing the same job at both ends. It was never "a prototyping tool that struggles in production." It's a runtime that deletes the part of the work that was identical in both places, and leaves you the part that was always the point.

The three-file demo wasn't the product. It was the smallest honest look at the product. File #4 is where it gets interesting — and file #4 is still config.

Try it

  • Repo: https://github.com/matutetandil/mycel
  • File #4, for real: the auth example — persistence + JWT + MFA + brute-force + sessions + audit, in config
  • Docker: ghcr.io/matutetandil/mycel

If you read the last post and thought "nice toy," this one's my rebuttal — and I'd still rather have you try to poke holes in it than nod along. That's genuinely how it gets to production-grade. Next post: what happens to a service like this when the power actually goes out.

Top comments (0)