Skip to content

Which architecture fits?

The right architecture is rarely the most impressive one.

It’s the one whose costs match your problem.

Not every application needs microfrontends, microservices, event streaming, a modulith, CQRS, BFFs, API gateways, and three distributed teams with a Conway diagram on the wall.

But not every application stays a simple todo app.

And that’s exactly where architecture begins.

Not with the question:

“What’s modern?”

But with the much more uncomfortable question:

“What complexity do we actually have — and what complexity are we just buying out of nervousness?”

The right architecture isn't the biggest. It's the one that actually holds up.

The myth: we choose architecture based on an idealized end state

Section titled “The myth: we choose architecture based on an idealized end state”

A popular line in projects goes:

“Let’s just build it right from the start.”

That sounds reasonable.

Until you notice that “right” often means:

  • the way it was shown at the last conference,
  • the way the big tech company described it in their blog,
  • the way the tooling is currently trying very hard to sell it,
  • or the way the architecture would look if the product had already grown for five years.

The problem: architecture isn’t a wish list.

Architecture is a bet.

A bet on where a system will change. A bet on which parts have to stay independent. A bet on which teams can carry which responsibility. A bet on which complexity operations can genuinely handle.

And like with any bet, you can be wrong.

Except with architecture, the penalty usually isn’t a lost stake — it’s a system that, after six months, looks like someone poured enterprise patterns into a coffee machine.

Architecture addresses problems — and creates new ones

Section titled “Architecture addresses problems — and creates new ones”

No architecture style is free.

A monolith isn’t automatically bad. A modulith isn’t automatically old-fashioned. Microfrontends aren’t automatically flexible. Microservices aren’t automatically scalable. Multi-tenancy isn’t automatically just a tenantId field. Cross-platform isn’t automatically “we’ll make it responsive with CSS.”

Every architecture style addresses certain problems.

And every one brings new problems along with it.

That’s not a flaw. That’s the deal.

Architecture decisionTypically answersTypically creates
Simple monolithfast delivery, low operational cost, easy consistencygrowing coupling if boundaries are missing
Modular monolith / modulithclear domain boundaries without distributed operationsdiscipline around module boundaries and dependencies
Microfrontendsindependent frontend teams and deploymentsruntime complexity, integration cost, UX consistency problems
Self-contained systemsend-to-end autonomy for business capabilities, including UI, API, dataplatform, observability, and operational overhead
Microservicesindependent services, scaling, technical decouplingdistributed data, network effects, failure modes, operations
Multi-tenancymulti-tenancy, data isolation, product variantspermissions, data modeling, tests, migrations
Cross-platform / multi-UImultiple clients and usage contextsAPI design, state models, design systems, release coordination

The question is not:

“Which architecture is good?”

The question is:

“Which problems do we have to solve — and which new problems can we afford?”

Architecture doesn't solve problems for free. It trades problems.

The simple todo app deserves simple architecture

Section titled “The simple todo app deserves simple architecture”

The todo app gets misused a lot in architecture conversations.

Either as proof that everything is trivial:

“It’s just CRUD.”

Or as a playground for demonstrating microservices, event sourcing, CQRS, DDD, Kafka, Kubernetes, and three frontend layers.

Both are nonsense.

A simple application deserves simple architecture.

That’s not dismissiveness. That’s respect for the problem.

If an application has a straightforward domain, gets built by a small team, has little integration pressure, doesn’t need complex role models, and can live fine with one shared deployment, then a cleanly structured monolith is often completely sufficient.

Maybe even the best solution.

Not because monoliths are magically good.

But because they create little operational overhead, enable simple local development, don’t spread data consistency across network boundaries, and don’t force teams to build a platform before the product even exists.

A bad monolith is bad.

A simple monolith with clear structure is a strength.

The difference isn’t in the name.

The difference is in the boundaries.

At some point, “we’ll just squeeze that in there too” stops being enough.

That rarely happens on one dramatic day.

It happens gradually.

First a feature gets spread across three components. Then a dialog hangs off a store that’s already serving five other use cases. Then the template suddenly knows API details. Then permissions get checked in places nobody expected anymore. Then a change to “edit customer” needs adjustments in five areas that, according to Jira, supposedly have nothing to do with each other.

Welcome to architecture’s everyday reality.

From this point on, a bigger architecture isn’t automatically necessary.

But a more deliberate one.

What matters more then:

  • domain-aligned feature boundaries,
  • clear modules,
  • facades,
  • explicit boundaries,
  • stable ViewModels,
  • clean stores,
  • tests in the right places,
  • dependency rules,
  • traceable data flows,
  • and clear domain ownership.

That’s when architecture stops looking like folder structure and starts looking like a system designed for change.

Growth doesn't immediately demand distribution. But it does demand boundaries.

The modulith: the underrated middle ground

Section titled “The modulith: the underrated middle ground”

Between “everything in one pot” and “distribute everything across network boundaries” sits an area that gets too little attention in a lot of projects:

The modulith.

A modulith isn’t a monolith with prettier folder names.

The idea is: a system stays deployable as one unit, but is clearly modularized internally. Domain areas have defined boundaries, dependencies are controlled, and technical and domain responsibilities are visible.

That can pay off significantly.

You get many of the benefits of clear architecture boundaries without immediately buying into the full cost of distributed systems.

No service mesh. No distributed transactions. No ten deployments for one small domain change. No debugging safari through five containers, three logs, and a dashboard that just says: “Something’s red.”

A modulith is especially interesting when:

  • the domain is growing,
  • one team or several teams need clear domain areas,
  • shared deployments are still acceptable,
  • data consistency stays important,
  • operations should stay lean,
  • and you still want to prevent the system from turning to mush internally.

The modulith, then, is often not a step backward.

It’s often the architecture a lot of systems actually needed, before someone said “microservices.”

A dedicated deep-dive article on this is worthwhile.

Microfrontends sound wonderful at first.

Independent teams. Independent deployments. Their own release cycles. Clear product areas. Technological freedom. Less coordination.

In the right context, that’s valuable.

For example, when several teams work on clearly separated domain areas, different product lines exist, deployments need to happen independently, or individual frontend parts are genuinely owned independently at the organizational level.

But microfrontends don’t fix bad boundaries.

They just make bad boundaries more visible.

Before, the coupling was uncomfortable in the code. Afterward, it’s additionally visible in runtime, routing, shared dependencies, auth, styling, the design system, API contracts, build pipelines, and deployment.

Congratulations. The problem now has more places where it can catch fire.

Microfrontends aren’t a repair kit for an unclear domain.

If it’s not clear who owns a feature, which data is allowed to be shared, which UI states are shared, and how domain responsibility is divided, then Module Federation doesn’t help either.

Then you don’t gain any architecture.

Then you have distributed ambiguity.

Distribution doesn't automatically make boundaries good. It just makes them more expensive.

Self-contained systems: autonomy is more than having your own build pipeline

Section titled “Self-contained systems: autonomy is more than having your own build pipeline”

Self-contained systems go one step further.

A domain area doesn’t just own a piece of frontend — typically also an API, data storage, business logic, deployment, and operations. The idea is strong domain autonomy.

That can make sense when domain areas genuinely need to deliver, change, and operate independently.

Not just on paper.

But with real teams, real responsibility, real operations, and real boundaries.

A self-contained system is attractive when:

  • domain areas are clearly separated,
  • teams carry end-to-end responsibility,
  • releases have to happen independently,
  • data ownership per area matters,
  • different requirements exist for operations or scaling,
  • and integration is meant to run deliberately through defined contracts.

But adopted too early, it quickly turns into a platform circus.

Then you’re no longer building a product — you’re dealing with shells, gateways, auth concepts, observability, deployment matrices, contract versioning, local development environments, and the question of why one area works in preview and the other only runs on Marco’s machine.

Self-contained systems are strong when the organization and the domain fit them.

They’re painful when you build them only because “autonomous teams” looked good on a slide.

Another classic:

“Multi-tenant? We’ll just handle that with tenantId.”

That’s one of those sentences where architecture briefly glances out the window and wonders whether it’s still needed today.

Of course a tenantId can be part of the solution.

But multi-tenancy is rarely just a field.

It’s about questions like:

  • Does data have to be strictly isolated?
  • Are there different role and permission models per tenant?
  • Is there tenant-specific configuration?
  • Different workflows?
  • Different designs?
  • Different data retention?
  • Different integrations?
  • Different release approvals?
  • Different regulatory requirements?
  • Shared or separate databases?
  • How do migrations work?
  • How do you test tenant-specific behavior?

A login with roles isn’t a multi-tenant architecture yet.

An application where different customer groups happen to use the same code isn’t a viable multi-tenant platform yet either.

Multi-tenancy becomes an architecture driver once data isolation, configuration, permissions, variants, and operations can no longer happen on the side.

And then you have to take it seriously.

Not in a panic.

But seriously.

Cross-platform doesn’t mean “responsive UI”

Section titled “Cross-platform doesn’t mean “responsive UI””

It’s similar with cross-platform.

A web app that doesn’t completely fall apart on a phone isn’t automatically a cross-platform strategy.

Multiple clients change a system.

A desktop web client has different interactions than a mobile app. A public consumer app has different security requirements than an internal admin tool. A kiosk mode, a native app, a portal, and a back office aren’t simply four screen sizes.

Multiple UIs affect:

  • API design,
  • ViewModels,
  • permissions,
  • error handling,
  • offline capability,
  • caching,
  • navigation,
  • testing,
  • release cycles,
  • the design system,
  • support,
  • monitoring,
  • product ownership.

Cross-platform becomes an architecture driver once different clients don’t just look different, but have different usage models.

Multi-UI frameworks aren’t an end in themselves either.

Angular, React, native apps, Web Components, and shell architectures don’t automatically answer how domain behavior stays consistent, which UI elements are shared, where variants are allowed, or how teams roll out changes safely.

Technology can help.

But technology doesn’t replace a decision.

More variants mean more architecture — not just more CSS.

“We’re designing for scale from day one” often means “We’re making it complicated from day one”

Section titled ““We’re designing for scale from day one” often means “We’re making it complicated from day one””

Scalability matters.

But a lot of systems don’t die because they don’t scale horizontally enough.

They die because nobody understands anymore where a domain change belongs.

Or because the team needs three days just to get everything running locally.

Or because every small tweak has to go through five repositories, two pipelines, three reviews, and one round of coordination.

Or because operations look professional in theory, but in practice nobody knows why login in preview only sometimes works.

“Scalable” isn’t a free pass for maximal architecture.

Scalable for what?

For user load? For teams? For features? For tenants? For releases? For integrations? For regulatory requirements? For ten years of maintenance?

Those are different kinds of scaling.

And they need different answers.

An architecture that scales for team autonomy can be unnecessarily heavy for small teams. An architecture that scales for user load can still be a swamp of tangled domain logic. An architecture that scales for multiple product lines can be absurdly expensive for an internal business application.

Scalability without a concrete context is just a fancier word for gut feeling.

Architecture isn’t a purely technical decision

Section titled “Architecture isn’t a purely technical decision”

Another myth:

“Architecture is a technical decision.”

No.

Architecture is always also a team, product, operations, and management decision.

Of course technical factors play a role.

Frameworks, runtimes, APIs, databases, build systems, deployment, security, observability — all of it matters.

But architecture also decides:

  • which teams depend on each other,
  • who has to approve changes,
  • which parts are owned jointly,
  • where product variants are allowed to emerge,
  • how fast new features can be delivered,
  • which failures stay isolated,
  • how expensive operations get,
  • and how long new developers need before they can meaningfully contribute.

If management wants an architecture that enables independent teams, those teams actually have to exist.

If a company wants microservices but only has one team handling operations on the side, that’s not a modern architecture.

That’s a hobby with a pager.

If a product wants to sell multi-tenancy, it has to understand multi-tenancy as a product capability — not as an afterthought database column.

Architecture can’t magic away organizational problems.

It can make them visible.

Sometimes that’s uncomfortable enough on its own.

Architecture shapes how technology and organization work together.

Instead of choosing architecture by buzzword, a sober map helps.

Not perfect. Not final. But better than “that’s what everyone’s doing right now.”

How much real domain complexity does the system contain?

Is this about simple data maintenance? Or about rules, variants, workflows, roles, status transitions, calculations, checks, and exceptions?

The higher the domain complexity, the more important clear models, boundaries, use cases, and tests become.

But domain complexity alone doesn’t mean microservices yet.

It often first means: better modularization.

Which parts change often?

Do UI flows change constantly? Do domain rules change? Do integrations change? Do tenant requirements change? Do product lines change independently of each other?

Architecture should carry the foreseeable directions of change.

Not the fantasies of some future platform empire.

How many teams actually work on the system?

A small team rarely needs the same architecture as five independent product teams.

If teams are supposed to deliver independently, boundaries, responsibilities, and deployments have to match that.

But if every change goes through the same three people anyway, maximum distribution often just produces maximum coordination.

Is there one UI?

Or several?

Admin, consumer, back office, mobile, kiosk, partner portal, internal tools?

Multiple clients are a strong architecture driver when they have different usage contexts, permissions, data needs, and release cycles.

Then BFFs, API boundaries, design systems, and shared model boundaries become interesting.

Not because they sound nice.

But because they can reduce real friction.

Are there really tenants?

Or just users with roles?

Does data need to be isolated? Do workflows need to vary? Does configuration need to be possible per customer? Do releases or features need to be controlled per tenant?

The more multi-tenancy shapes the product, the earlier it has to be modeled architecturally.

Adding tenantId later, “just like that,” is about as relaxed as changing an axle while driving.

How many external systems hang off this?

Are these systems stable? Or do they change often? Are the data models foreign? Are there legacy systems? Are there asynchronous processes? Is there domain translation at system boundaries?

High integration pressure argues for clear adapters, anti-corruption layers, robust interfaces, and good observability.

Not automatically for maximum distribution.

But definitely against “we’ll just pass the DTOs straight through into the template.”

Do parts need to be independently deployable?

Really?

Not “would be kind of cool.”

But: are there domain, organizational, or regulatory reasons why area A has to go live while area B stays unchanged?

If yes, microfrontends, self-contained systems, or microservices become more interesting.

If no, a shared deployment is often simpler, safer, and cheaper.

8. Operational and observability requirements

Section titled “8. Operational and observability requirements”

Can the team operate the architecture?

Logging, tracing, metrics, alerts, failure analysis, rollbacks, data migrations, local development, preview environments, security patches.

Distributed systems without observability aren’t a modern architecture.

They’re a smoke machine.

The more distributed a system is, the more operations has to be considered from the very start.

Not later, once the first failure only shows up in the browser as a “504.”

Is this a short-lived tool?

An MVP?

An internal form?

A platform meant to last ten years?

Long-term systems need different investments in structure, tests, modularity, documentation, and ownership.

But the same rule applies here too: being long-term doesn’t justify every complexity right away.

You can build architecture to be developable without simulating the end state on day one.

10. Regulation, permissions, and data isolation

Section titled “10. Regulation, permissions, and data isolation”

Are there high requirements around data protection, auditability, permissions, or data isolation?

Then architecture isn’t just a question of code quality.

Then it’s about trust, traceability, access control, and containing failures.

That affects data models, APIs, logging, tests, role models, tenant boundaries, and deployment strategies.

And yes: that’s more uncomfortable than “we’ll do security later.”

The last question is the most uncomfortable one:

What architectural complexity can we actually carry today?

Not theoretically.

Today.

With this team. With these skills. With this budget. With this operational maturity. With this product uncertainty. With this time pressure.

Overengineering feels professional at first.

Later it feels like a toll booth in front of every change.

This table isn’t a rule.

It’s a map.

And like any map, it doesn’t replace looking out the window.

ContextOften suitable direction
Small app, small team, limited domain complexitysimple, cleanly structured monolith
Growing domain complexity, one or few teams, shared deployment acceptablemodular monolith / modulith
Multiple domain areas, clear team ownership, independent UI releases neededconsider microfrontends
Domain areas need to be end-to-end autonomous, including UI, API, data, and operationsconsider self-contained systems
Services need to scale, deploy, or own data independently along domain linesconsider microservices
Multiple tenants with isolation, configuration, variants, and permissionsdeliberately design a multi-tenant architecture
Multiple clients with different usage contextsconsider a cross-platform strategy, BFFs, design system
Many teams, shared UI language, recurring patternsa design system / UI platform makes sense
High integration pressure toward external systemsadapters, ACLs, stable API boundaries, observability

The important word is “consider.”

Not “adopt.”

Architecture isn’t something you order from a menu.

A better alternative: choose architecture for the pressures it must handle

Section titled “A better alternative: choose architecture for the pressures it must handle”

A better architecture decision doesn’t start with:

“Do we want microfrontends?”

But with questions like:

  • What needs to be changeable independently?
  • What needs to be deployable independently?
  • What needs to stay separated along domain lines?
  • What needs to stay together?
  • Which data genuinely belongs together?
  • Which parts change most often?
  • Which teams own which area?
  • Which UI parts need to stay consistent?
  • Which variants are a product capability, and which are just edge cases?
  • Which complexity can we actually operate today?
  • Which complexity do we actually need?
  • What happens if we’re wrong?
  • Can we grow sensibly later without prebuilding everything today?

These questions are less glamorous than architecture diagrams with a lot of boxes.

But they keep you from building an architecture that says more about what the team wanted than about what the product needs.

Don't choose the pattern. Understand the pressures.

What later articles will look at more closely

Section titled “What later articles will look at more closely”

This article is deliberately an overview.

Each individual architecture style deserves its own treatment, because each one has its own strengths, traps, and decision questions.

Planned deep-dive articles:

  • Monolith When a shared system is a strength — and when it tips over.

  • Modulith How clear domain boundaries can work without distributed operations.

  • Microfrontend When independent frontends make sense — and when they just produce distributed chaos.

  • Self-contained system What end-to-end responsibility looks like and what maturity it requires.

  • Microservices Why service boundaries have more to do with data, ownership, and operations than with the number of repositories.

  • Multi-tenant architecture Why multi-tenancy is a product capability — not just a database field.

  • Cross-platform frontends What multiple clients actually mean for APIs, ViewModels, the design system, and releases.

  • BFF / API gateway When client-facing APIs help and when they just become another pass-through.

  • Design system / UI platform When shared UI structure speeds teams up — and when it becomes a central bottleneck.

Good architecture isn’t as large as possible.

It’s big enough for the problem — and small enough to still be understood.

It carries the foreseeable directions of change. It fits the team structure. It respects operational reality. It separates what needs to be separated. It holds together what belongs together.

And it has the courage to be no more impressive than necessary.

Good architecture isn’t as large as possible. Good architecture is big enough for the problem — and small enough to still be understood.