When I started designing ClientForth, it would have been easy to divide the application into several services. Leads, estimates, projects, messages, and payments all look like natural service boundaries. Microservices would also have made for an impressive architecture diagram.
But an impressive diagram was not the problem I needed to solve.
The real problem was building a useful product, learning how service businesses actually move work from an inquiry to a completed payment, and changing that workflow quickly as I learned more. A distributed architecture would have added deployment, networking, observability, and data-consistency work before the product had earned that complexity.
The decision
ClientForth began as a modular monolith: one deployable application with intentionally separated product areas.
The distinction between a modular monolith and an unstructured monolith matters. “One application” should not mean that every feature can reach into every other feature. Leads, estimates, projects, and billing still need clear responsibilities. Business rules belong near the part of the system they describe, and shared code should be genuinely shared—not simply placed in a common folder because its owner is unclear.
This gives me most of what I need today:
- one build and deployment path;
- straightforward local development;
- database transactions without distributed coordination;
- easier debugging across a complete user workflow;
- and room to change boundaries while the product is still evolving.
Why it fits an MVP
An MVP is a learning system. Its first architecture should optimize for safe learning and reliable delivery.
For ClientForth, a customer action may touch several parts of the product. Accepting an estimate could update the lead, create a project, schedule follow-up work, and eventually connect to a payment. Keeping those operations together makes the workflow easier to understand and allows related database changes to succeed or fail as one transaction.
It also keeps operations modest. I can deploy one application, follow one set of logs, and reproduce most problems locally. That is valuable when the same person is designing the product, writing code, reviewing AI-assisted changes, testing releases, and operating the application.
The tradeoff
A modular monolith does not enforce its boundaries for me. It requires discipline.
Imports can gradually cross boundaries. A convenient database query can bypass the intended module. Shared utilities can become a place where unrelated business logic accumulates. I have to review for those problems in the same way I review for security, correctness, and test coverage.
There is also a chance that part of the system will eventually need independent scaling or stronger isolation. Background communication work, document generation, or payment processing could become candidates. I am comfortable with that possibility because extraction is easier when the original module already owns clear data and behavior.
What would justify a separate service?
I would not extract a service because the codebase reached an arbitrary size. I would look for a concrete operational reason:
- a workload needs to scale differently from the main application;
- a failure must be isolated to protect the core workflow;
- deployment frequency is being constrained by unrelated work;
- a module needs a different runtime or storage model;
- or ownership has grown enough that teams are blocking one another.
Until one of those pressures is real, a separate service would mostly move complexity from code into infrastructure.
The principle I am keeping
Architecture is not a prediction contest. I cannot know every direction ClientForth will take, but I can make today’s system understandable and keep tomorrow’s options open.
For this stage, a modular monolith is not a shortcut or a temporary embarrassment. It is a deliberate choice: clear internal boundaries, simple operations, and more time spent improving the product customers will actually use.