Founder Notes
Composable Architecture Without Client-Heavy Bloat
Why small teams should stay server-first longer and separate modules before services
Founder Notes
Why small teams should stay server-first longer and separate modules before services
Why most small teams should start server-first, stay modular, and stop confusing code boundaries with network boundaries.
A lot of teams say they want a composable architecture.
What they often build instead is distributed coupling.
They split the app into a separate frontend and backend early, push everything through HTTP, duplicate validation and state handling on both sides, and call it flexibility. In reality, they did not remove coupling. They just moved it into JSON payloads, API contracts, and deployment coordination.
That is not composability. It is overhead.
For most products, especially those built by small teams, a better default is much simpler:
This gives you a system that is easier to ship, easier to reason about, and still flexible enough to grow a new UI or new interfaces later.
This is the root confusion.
People see a clean architecture diagram and assume the boxes must be separate processes talking over HTTP.
They do not.
A boundary in code does not automatically mean a boundary over the network.
Those are two different questions:
Most teams answer question two far too early.
You can have a well-structured, modular app where the parts are cleanly separated in code but still run inside one deployable system. In fact, that is usually the better starting point.
Server-first does not mean shoving business logic into templates and calling it a day.
It does not mean building a giant tangled monolith where controllers, views, and database models all leak into each other.
It means:
That last point matters most.
The goal is not “HTML everywhere forever.”
The goal is to keep the core of the app stable, while making it possible to attach different adapters around it.
Those adapters might be:
If those are all thin layers over the same application core, you have optionality without paying the full cost upfront.
When developers say “composable,” they often mean one of two things.
The first is infrastructure composability:
The second is product composability:
Both are valid goals.
But neither one requires you to split into “frontend” and “backend” as separate systems from the beginning.
What you actually need is a stable application core and clean seams around it.
For most business apps, the right default is not a fat client and not a microservice fleet.
It is a modular monolith.
One deployable app. Clean internal modules. Clear boundaries. Thin adapters.
That means the system is structured around things like:
Not around a premature split into two independently deployed apps.
A simple shape looks like this:
/domain
listing.ts
billing.ts
permissions.ts
/application
createListing.ts
approveListing.ts
chargePlan.ts
/ports
ListingRepo.ts
BillingGateway.ts
SearchIndex.ts
/adapters
/db
ListingRepoPostgres.ts
/web-html
listingsPage.ts
/web-api
listingsApi.ts
/jobs
reindexListings.ts
The important part is not the folder names. The important part is the direction of dependency.
The application core should not care whether the request came from an HTML page, an API route, or a worker.
Imagine you are building a listings product.
You have:
A lot of teams would jump straight to:
But if you are a small team, that usually creates more problems than it solves.
A cleaner shape is this:
CreateListing application serviceListingRepo interfaceBillingAccess policy/serviceHtmlListingsControllerApiListingsControllerThen the flows look like this:
HtmlListingsController -> CreateListing -> ListingRepo -> DB
ApiListingsController -> CreateListing -> ListingRepo -> DB
AdminJob/Worker -> CreateListing -> ListingRepo -> DB
All three use the same business capability.
That is real composability.
You can redesign the UI later. You can add an API later. You can migrate route by route. You can even remove the HTML controller for one area if a richer frontend becomes worth it.
The core stays intact.
This is where many people get lost.
If you have these parts inside the same application:
then they usually should not communicate via HTTP.
They should call each other directly in-process.
Like this:
HtmlController -> AppService -> Repo -> DB
not like this:
HtmlController -(HTTP)-> AppService -(HTTP)-> Repo
HTTP is for process boundaries.
Inside one app, use direct calls.
This seems obvious once stated plainly, but a lot of teams blur the line between a conceptual boundary and a network boundary.
That confusion creates fake microservices inside one product.
The result is predictable:
You bought distributed systems problems without earning the benefits.
Most core business modules should stay in the modulith longer than people think.
Keep a module in-process when most of these are true:
This usually applies to things like:
These are not great early service boundaries. They are usually core modules of the same application.
Separate services make more sense when something is operationally distinct.
That usually means it is:
Examples:
These are much better candidates for workers or separate services.
The pattern here is simple:
If a module mostly answers what the business does, keep it in the modulith by default.
If it mostly answers how the system processes heavy or operationally distinct work, it is a better extraction candidate.
This is the point people are usually worried about.
They want to avoid getting trapped in one presentation layer.
Fair concern. But the solution is not to split everything on day one.
The solution is to keep the presentation layer thin.
If your server-rendered HTML controller is only an adapter that calls application services, you can later:
That is often the best real-world setup.
For example:
Not every page needs the same architecture.
That is another reason to resist defaulting to an all-client app too early.
Teams usually do not split early because they are stupid. They split early because the story sounds reasonable.
“We want flexibility.”
“We might need mobile later.”
“We do not want a monolith.”
“We want the frontend to be replaceable.”
All of that sounds smart. The problem is that these are often hypothetical benefits traded for immediate, certain costs.
Those costs are real:
You should not pay those costs based on imagined future consumers.
Build the second consumer when it becomes real.
Until then, keep the architecture honest.
The right progression for most products looks like this:
One app. Clear boundaries. Server-first by default. Thin controllers.
Maybe a richer frontend is justified for one area. Fine. Add the API adapter there.
Workers, indexing, AI jobs, media processing, webhook handling.
That pressure might be:
Until then, keep the domain close together.
If you are unsure, default to this:
Keep business logic and core workflows in one modular, server-first app. Extract only the parts that are operationally distinct, async, or heavy.
That rule will save most small teams from a lot of self-inflicted architecture pain.
A composable architecture is not one where everything talks over HTTP.
It is one where the core of the app remains stable while the surfaces and infrastructure around it can change.
That means:
That is the boring answer.
It is also the one that usually works.