Microservices, and the work I hadn’t accounted for
A meetup conversation, a RabbitMQ timeout, and what they taught me about choosing a distributed architecture.
At a Reggio Emilia Fullstack meetup, someone argued that a couple of lines of Bash could be enough to deploy a monolith. I remember the discomfort that idea caused. We were there to talk about architecture, and here was someone suggesting we might be making it harder than it needed to be.
That conversation stayed with me. So did the evenings spent with RabbitMQ, container builds, and Azure DevOps. I still like working on these systems. I’m just more careful now about which complications I choose to take on.
The RabbitMQ connection that wouldn’t stay connected
I spent an evening following a RabbitMQ connection that kept disconnecting and reconnecting. Messages were flowing, the logs looked reassuring, and then the connection would drop again. That regularity was the clue.
The client used WebSockets, and a proxy idle timeout was part of the problem. I had started by looking at the client library. I needed to look at the path the connection took and what kept it alive.
Another issue involved services declaring the same exchange with different settings. Configuration such as durable and autoDelete forms a contract between producers, consumers, and the broker. A mismatch can cause a declaration to fail with PRECONDITION_FAILED and close the channel.
Neither issue looked particularly interesting on an architecture diagram. Both mattered when the application had to run. Connection recovery and consistent broker configuration now get more of my attention upfront.
The build environment is part of the system
Native dependencies complicate otherwise familiar Node.js deployments. Libraries such as node-canvas depend on system libraries, and a binary built for one environment may not run in another.
Container images need deliberate choices about the operating system, native dependencies, and build tools. Multi-stage builds can separate compilation tools from the runtime image, but the build still needs to be reproducible.
Windows container work also taught me that image and filesystem problems can require platform-specific investigation. Development and production differences should be understood and tested, rather than discovered during a release.
Delivery needs coordination
Independent services do not remove dependencies between releases. An API change, a missing environment variable, or an incompatible combination of versions can still break a user workflow.
I have worked with Azure DevOps and self-hosted agents. They provide control over the build environment, along with responsibility for maintaining it:
- Keep runtime and build-tool versions consistent.
- Maintain container tooling and native dependencies.
- Monitor disk usage and remove obsolete build artefacts safely.
- Make configuration and release steps repeatable.
The pipeline is part of the application’s maintainability, not an afterthought once the services exist.
Logs need a shared context
An error spanning multiple services is difficult to investigate if each service writes unrelated text in a different time zone.
Structured logs, consistent timestamps, and correlation IDs make it possible to follow a request. Centralized collection gives those records a common place to be searched. The exact platform matters less than whether the records answer practical questions: which request failed, where it went, and which dependency was involved.
Choose boundaries before distribution
My starting point for many applications is a modular monolith: explicit module boundaries within a single deployment. Those boundaries still deserve clear interfaces and tests.
Separate services become useful when there is a concrete reason, such as independent release needs, different resource requirements, or isolation constraints. They also require a plan for configuration, messaging failures, observability, and operations.
I still think about that meetup when I’m looking at an architecture diagram. Before adding another service, I want to be able to explain which problem it solves, and who will be looking after it.