Is This Modular Monolith Architecture Good? or a Massive Future Mistake?
Posted by UnderWolf1@reddit | ExperiencedDevs | View on Reddit | 39 comments
I’m trying to avoid both:
- giant monolith spaghetti
- and premature microservices
So I ended up designing something that’s basically a feature-oriented modular monolith inside a monorepo.
Operationally it’s still:
- one app
- one deployment
- one runtime
- one shared DB
But internally, everything is split into isolated full-stack feature modules/plugins like:
- auth
- file-manager
- payments
- templates
Each module owns its own:
- DB tables/migrations
- routes/API
- services/business logic
- permissions
- SDK/client
- React providers/hooks
- UI components
Example:
<FileManagerProvider>
<FileBrowser />
<FileUploadDialog />
</FileManagerProvider>
Where:
- FileBrowser and FileUploadDialog belong to the file-manager module
- they internally use file-manager hooks/providers/state
- they may call fileManagerSdk.listFiles()
- which talks to file-manager backend routes/services
- which own the actual file tables/storage/permissions
So the frontend/backend are owned together as one vertical feature slice instead of everything becoming shared global state and shared service spaghetti.
The main app mostly just composes modules together:
const auth = createAuth(...)
const fileManager = createFileManager(...)
app.use('/auth', auth.router)
app.use('/files', fileManager.router)
Cross-module DB references are allowed because it’s still one DB/app.
Example:
foods.image_id REFERENCES files.id
But business behavior still stays inside the owning module.
So another module can reference files.id but file permissions still go through file-manager services.
A major goal of this architecture is feature reusability.
I want modules/plugins to be installable across multiple apps/projects with minimal coupling.
For example:
- reuse the same auth module in multiple apps(including ready-made auth templates/components for sign in, sign up, forgot password, reset password, etc)
- reuse the same file-manager in admin/customer/internal apps
- reuse the same React components/hooks/providers together with the backend handling they belong to
So instead of reusing only “dumb UI components”, the goal is reusing entire full-stack capabilities/features.
This feels cleaner/scalable to me than both:
- giant shared monoliths
- and early microservices
But I’m wondering if this is one of those architectures that sounds elegant and later becomes painful.
Has anyone here built systems like this long-term?
Where does this architecture usually fail?
Colt2205@reddit
It depends on the needs to scale it and the how readable the code really is.
My own style and preference is to break responsibility by entry point. So there might be an API system that is for users to run reports, but then the ETL pipeline for getting the data for the reports is a separate entry point and its own software.
So if I saw someone use something like springboot with profiles to accomplish the same task and throw all that code into the same code base I'd say that is done incorrectly, because it was a shortcut and now code for the API portion is mixed with code that is meant to be used for the ETL. That goes doubly so since regardless of being in the same software package multiple processes are going to have to run in parallel doing completely different tasks with large amounts of dead code in each piece.
Separate front and backend responsibility with fullstack is the part that starts getting into debates. There's no dead code argument because API systems tend to spider out a lot in function. Separate APIs with dedicated responsibilities linked to a front end can work, or having multiple websites dedicated to specific tasks all linked together through a central hub page.
There's just a lot of ways to skin a cat and the most important thing to me is remembering someone else has to eventually learn how this crazy thing works, and if we did a good enough job building it so they can. Anything beyond that is outside our control, including what people do with that knowledge. I've come to conclude that everything is a trade of problems. The frustrating part is when a trade isn't necessary.
ArtSpeaker@reddit
If the modules are already organized, separated, and effecient. Then the remaining difference is whether those modules sit inside the one app or several.
That means trading the ability the independently scale those modules with the need to auth + transform the data at every app boundary.
And that only you can know.
akie@reddit
You also don’t have the network and infrastructure overhead, and tracing and logging is a lot easier.
ArtSpeaker@reddit
Absolutely. Logging issues much easier, but it changes the tooling. I think. I imagine there is... something... lost when all metrics are all under one app. Maybe we have already figures that out and I just haven't seen it.
rincewinds_dad_bod@reddit
Yes but the tools are free vs expensive
Instrumentation on the coffee level vs the node level does increase coupling and especially if you are on a niche stack, it might be limited
But, the metrics are simpler, and more accurate. So troubleshooting is much much much much more straightforward.
Imo the cost savings is on that side of it all.
New-Locksmith-126@reddit
The hard part is not choosing monolith or microservices. The hard part is writing well organized modules with clear contracts. If you skip that, it becomes a rat's nest either way.
ArtSpeaker@reddit
Agreed. But Op said they did that. I took their post as a "final mile" concern.
New-Locksmith-126@reddit
The hard part is not choosing monolith or microservices. The hard part is writing well organized modules with clear contracts. If you skip that, it becomes a rat's nest either way.
Isogash@reddit
You don't really need the ability to independently scale modules in most cases. You are only paying the memory cost for code once per instance, and memory/CPU for use is only when that module gets used, to the extent it's used.
UnderWolf1@reddit (OP)
I get what you mean.
I think my point is more specific than just having well-organized modules in global folders.
What I’m trying to design is closer to installable, self-contained feature packages inside the monorepo that deliver full-stack capabilities.
For example, an
authpackage would not just be a folder with auth-related code. It would ship the whole auth capability:Then an app can install/use that capability and still customize parts of it through extension hooks:
And on the frontend, the app can import fully working auth UI flows directly from the same package:
Those templates are not just dumb UI components. They already contain the auth flow logic and work with the auth API/plugin out of the box.
For example, the package could already support:
Internally:
And the app can still customize behavior either:
onUserCreateSo the thing I’m asking about is not only:
“Can the code be organized into modules?”
It’s more:
“Can full-stack features be packaged as self-contained, reusable capabilities across apps without slowly becoming coupled to app-specific assumptions?”
That’s the part I’m trying to validate long-term.
ArtSpeaker@reddit
> “Can full-stack features be packaged as self-contained, reusable capabilities across apps without slowly becoming coupled to app-specific assumptions?”
Yes. Of course they can. That's just 3rd party libraries, but all parties are you. You can avoid app assumptions with configs, but you cannot avoid system assumptions without some mean tradeoffs. Especially at scale.
Just your metrics alone require their own system-wide organization. And is Auth truly independent if it requires the same logging and DB your app does?
Spring Boot tackles these challenge exactly. You should look into how they do it.
Regardless, this is going to be a huge experience for you. Good luck.
DeterminedQuokka@reddit
Any architecture is good if you do it right and bad if you do it wrong.
The positive of a modular monolith over micro services is it can be easier to fix boundary mistakes.
But it’s also way easier to just call across boundaries. You need good linting to stop that.
Flashy-Whereas-3234@reddit
I live inside one of these "theoretically separated" monoliths, and were at a size where we want to action full separation, so here's my take.
Theory is great, but if you can violate the rules, people will, and then you're fucked. You'll want something which enforces (eg. Linting) the kind of regimental separation you expect.
If you see a dumping ground forming, get angry early, make sure everything is still a named SDK or Lib and you don't grow a "tools" bin.
Data separation is really very very hard, particularly where you have FKs or a shared ORM that'll happily bleed one domain into another. Each domain having its own lightweight representation of other domains data is the most micro-service-alike way to handle data, but monoliths and their ORMs don't tend to work this way. Consider this problem early, because if you DO force the modules to be separated in terms of data then they'll lay fewer turds on each other, with the drawback that it'll piss off Devs.
Go to events, message bus, and queues early. Don't let cross domain function calls propapagate. Domains can all asks but not each other. Again, enforcement of your separation rules are your friend.
Regimented, opinionated, but explain the "why" to your engineers. Gold plating these requirements early will hinder your time to market, but it allows you a cleaner separate system down the line.
More often than not the business will consider "giants system with too many users" to be a "nice problem to have if we ever get there".
TheTacoInquisition@reddit
I advocate for separate DB schemas where data is likely to need separation. Too many devs will absent mindedly stick a foreign key on a table, coupling domains together. And others will start an argument about query performance being more important, especially when it's a lazy way to avoid thinking about longer term solutions.
Separate schemas stops both early.
AI is compounding the problems as well. I had the exact issue where the agent really didn't like the separation of concerns and kept trying to bend and break the rules.
Isogash@reddit
FKs are overrated and unnecessary most of the time, you shouldn't use them across module boundaries.
Flashy-Whereas-3234@reddit
It's very contrary to what we were brought up on, but I absolutely agree. It's a necessity when you end up with event steaming CDC, and document stores couldn't give two shits anyway.
Isogash@reddit
Right, but there's also some theory behind not using FKs too. I don't have enough time to explain in full now, but basically if you view tables as being different facts about real entities, then all a FK is is a constraint that requires you to have one kind of fact before another, so it's not useful if you don't need that restriction.
FetaMight@reddit
In other words: model your db schema based on your actual needs rather than generic "best practices."
Isogash@reddit
Sure, that too, but there are also real ontological reasons why FKs are mostly unnecessary. Look into open vs closed world assumptions, most databases should use the open world assumption whilst FKs only make sense for closed world.
FetaMight@reddit
I think we're, more or less, saying the same thing.
If you model your schema on such a way that it enforced constraints that don't actually exist, then you're making a modelling mistake.
A lot of people assume schema modelling is like picking a recipe from a catalog of 3 recipes. In reality the right scheme can be far more subtle than what get repeated as "the right way."
Connect_Detail98@reddit
Every decision is a mistake depending on the context.
vismbr1@reddit
My rule of thumb is to start with a monolith unless there’s a very good reason not to. Scope creep is common, so it’s usually better to let the application mature a bit before deciding whether certain parts should become separate modules or microservices.
UnderWolf1@reddit (OP)
I get the idea of starting simple and not extracting microservices too early.
But my goal is a bit different: I’m not only trying to decide when to split a mature app into modules or services. I’m trying to avoid reimplementing or copy-pasting the same full-stack features across future projects.
For example, if I build a new app later, I want to be able to install an
authpackage and get the full auth capability:Then the app can compose that package into the monolith and customize it through backend hooks or frontend props.
So operationally it can still be one app, one deployment, one runtime, and one database.
But architecturally, I want features to be packaged as reusable full-stack capabilities from the start, so I don’t have to rebuild them for every new project.
That’s the part I’m trying to validate: whether designing for that kind of reuse early is a good long-term pattern, or whether it usually becomes painful as the apps evolve.
noharamnofoul@reddit
You’re overcomplicating it.
Dragon_ZA@reddit
So, youre trying to make your own libraries for all your internal apps? Are you planning on storing all these internal libraries and multiple apps in the same repo?
morswinb@reddit
Its all sounds good.
People is when it fails.
Microservices solve organizational issues, while creating techncial ones.
If your team is small, 5 people or less, and you have great understanding of the system functions you avoid a lot of unnecessary overhead, move fast and create value quickly.
However if you hit 50 people working in one monorepo things break down due to people being people. The technical changes become easier to overcome at this scale. Hence split services naturalny as team and domain grow.
Dont fall into the survivor bias of monoloth rewrite stories.
If your monolith gets a rewrite, then it was making some money to begin with.
If microservices first project makes no money, you don't publish a failure story.
andrewharkins77@reddit
Monolith versus Micro-Service is more about your company organization. Does your product have multiple domain experts, different users, many streams of working going in parallel? If so, split the product along domain lines. Especially, if the only thing each domain share is the database.
UnintentionallyEmpty@reddit
...why?
You expect to be building multiple apps that have the same functionality?
...why?
In my experience? Trying to reuse something because it's 80% like what you actually need and then having to build something ridiculously complex on top of it to get the other 20% correct, instead of just building something that actually fits the needs of the new application, which would have taken a fraction of the time.
thekwoka@reddit
This seems fine.
PatchSprite@reddit
This is essentially the "vertical slice" architecture and it's genuinely good until modules need to talk to each other in non-trivial ways. That's where the elegance starts leaking , cross-module workflows that don't cleanly belong to either module become a negotiation every time.
The other failure point is the shared DB. It feels fine now but cross-module foreign keys quietly become coupling you can't easily untangle later. Worth being stricter about that boundary than you think you need to be today.
originalchronoguy@reddit
What do you do when one of your plugin/module is better served by something outside of your traditional stack?
Say one service requires a GPU and another stack is better suited for GPU workloads vs CPU?
Empanatacion@reddit
I couldn't quite tell from your description, but you don't want separate modules directly accessing the same database tables. It sounds like maybe you just have entity IDs referring to one another, which is fine, but you don't want other modules to know or care if you rename a column, for example.
skuple@reddit
I would look at it as “Any module can be taken out at any moment as a separate service”
If for some reason (e.g performance, scale independently, external dependency) you need to extract one of the modules, it will be much easier if the path was already contemplated.
IMO monoliths are pretty good at speeding up development in the beginning but eventually if the project grows too much you might need to extract a couple things .
Not sure what’s the tech behind your project but you can also create the monolith in a monorepo where each module is it's own package called directly by the entry package (the app itself) which can be transformed into a service later on.
UnderWolf1@reddit (OP)
Yeah, this is very close to what I’m thinking.
The idea is not to start with microservices, but to structure the monolith so each major feature is its own package with a clear boundary.
For example, in the monorepo the app package would compose feature packages like:
But the part I’m also trying to optimize for is reuse across future projects.
So an
authpackage would not only be “extractable later as a service.” It would also be reusable as a full-stack capability in another app.It could expose:
Then another app could just import/plug it in:
And on the frontend:
The templates already contain the auth flow logic and communicate with the backend auth plugin through the SDK/provider.
And the app can still customize behavior through backend hooks or frontend component props/slots.
410_clientGone@reddit
this is distributed monolith and having worked in one before my advice is don't to it
GumboSamson@reddit
“Distributed monolith” usually means “tightly coupled microservices.”
I think OP is describing an actual monolith. (This isn’t an insult—monoliths are a valid architectural choice.)
PaulPhxAz@reddit
I usually make microservices by default. I'm not sure how everybody else does it, but I have a static internal SDK that talks to the message bus for me, and a standard Consumer class that receives the messages. The OLTP/ReqIds/Spans/JWT/Tokens, logging, tracing, whatnot is all handled by those two classes. Reflection to send/receive.
Given that, I bundle them together during deploy and redirect the SDK call to the local instance ( where it makes sense ).
So, you have incredibly separate service domains living in 1 deployable. AND if you want to scale just one particular service, you can deploy it independently.
But the "early microservices" being wrong is usually if you're team is doing it badly or hasn't tooled enough around it.... plus network latency. It's not the organization part or complexity part. It's just added latency.
I wouldn't share a single database... even if it was with it's own owned tables. But it also depends on how you're organized. Some projects work well with some large sprocs doing a backend process fully on the DB. You might want to scale that independently.
Routes/API --> I always use a HTTP to Message bridge... so there is nothing to "own". The SDK gets an attribute on whether it's exposed through the bridge. But it makes the idea of an API and Routes and Controllers irrelevant. Instead you have the Consumer, like you do for any message.
Services/Business logic. I do it like this: Ext Interface --> Orchestration --> Component ( Biz Layer ) --> Channel ( routing ) --> Sub-Channel ( direct integration )
Layers if needed.
Permissions --> One common module for these. Every use gets a Scope used and a set of Roles. The roles expand into "permissions" if needed. But I would keep everybody consistent. Your Auth/Security domain should handle this basic user stuff. Each domain/module can tag it's Pages/APIs/BizComponents with [AuthPolicy(Needs="Accounting Manager")] or whatever, and then you get your OAuth token in state, and ask the Auth Utility code to ask "Does this Token have access to this Role?".
SDK/Client --> I expose swagger/openapi standard and then docs. I don't usually make an external SDK. But you're mileage may vary here.
React providers/hooks & UI components --> I make basic sites and add HTMX interactivity. I think it's easier to start here.
EirikurErnir@reddit
What worries me most is seeing things that look like business features (payments) sharing a placement with things that look like non-differentiating technical boilerplate (file-manager/auth) to me.
The idea of modules that contain their own vertically integrated feature set is pretty good IME. The tricky part is to decide where those module boundaries should be, and there I would generally advise to look at the concepts that are most relevant to your business and try to focus on encapsulating those.
imsexc@reddit
Modulith is a valid architecture