Notes / 55onze

All notes

Why consistency is an architectural problem

Consistency in frontend systems is often treated as a visual problem, but the differences we see in an interface are frequently the result of architectural decisions made much earlier.

Foto de Roman na Unsplash
Foto de Roman na Unsplash

A few years ago, I worked on the frontend architecture of two large e-commerce platforms. They belonged to the same company and shared a significant part of their structure, but they were not the same product.

There were small differences between them. Some were visual, others behavioral, and most were legitimate product requirements. The challenge wasn't deciding whether those differences should exist, but understanding how to represent them in the codebase without gradually turning a shared frontend into two completely separate ones.

By that point, we had already moved the applications into a monorepo and were sharing components between both platforms. We were also using Atomic Design to organize the component structure.

From an architectural perspective, many of the things you would normally expect to improve consistency were already there. We had a shared repository, reusable components, and a common way of organizing the interface.

Even so, the component base kept growing.

Eventually, we had more than 300 components.

Some of that growth was expected. If two products genuinely need different behavior, forcing them into the same abstraction isn't necessarily better engineering. But as those small differences accumulated, the component base became increasingly difficult to understand and maintain.

A change that sounded simple could end up requiring another component. Something that was almost reusable could require just enough customization to make the abstraction uncomfortable. In other cases, trying to keep two slightly different implementations in the same component could make that component harder to understand than having two implementations in the first place.

Looking back at a system like this, it is easy to say that the abstraction should have been better.

The reality was more complicated.

We had a large frontend team, multiple squads working in parallel, and products that still had to evolve while we built the architecture. The team size, the number of products, the deadlines we were working with, and the coordination required between squads all influenced what kind of architecture was practical.

Delivery did not stop while we searched for the cleanest possible abstraction.

The architecture reflected those constraints, and that experience gradually changed the way I thought about consistency.

For a long time, I had associated consistency mostly with what appeared on the screen. If two applications had similar buttons, forms, or interactions, making those things look and behave the same whenever possible seemed like an obvious goal.

But when you work with a shared component base at that scale, the problem looks different.

What appears on the screen is often only the final consequence of decisions made much earlier, such as how responsibilities were divided, how abstractions were created, how much autonomy each team had, and how product differences were represented in the architecture.

The interesting question becomes less about whether two buttons look the same and more about why they became different in the first place.

Sharing components does not remove architectural decisions

Foto de Merrilee Schultz na Unsplash
Foto de Merrilee Schultz na Unsplash

A monorepo solves several useful problems.

It makes code sharing between applications easier, gives teams more visibility into what already exists, and can simplify changes that affect multiple products at once.

It does not, however, decide how shared code should evolve.

Imagine two products using the same component and one of them needs slightly different behavior.

The first change might look harmless:

<ProductCard variant="compact" />

Then another requirement appears:

<ProductCard variant="compact" showPromotionBadge />

Later, one of the products needs a slightly different interaction:

<ProductCard
  variant="compact"
  showPromotionBadge
  alternativeSelectionBehavior
/>

None of these changes are necessarily wrong.

Each can be perfectly reasonable in isolation.

The problem starts when a component gradually becomes the place where every difference between products is represented. At some point, the abstraction can become harder to understand than the duplicated implementation it was created to avoid.

The opposite approach creates a different problem. If every small difference creates another component, the shared library fills with nearly identical implementations, and developers must understand increasingly subtle distinctions before deciding which component to use.

That was part of the difficulty we were experiencing.

We were not struggling because we had forgotten to reuse code. We were already reusing a large amount of it.

The difficult part was deciding where reuse should stop.

The repository structure or the component itself cannot make that decision. It depends on the products, the teams maintaining them, how likely those products are to diverge, and how much complexity a shared abstraction can reasonably absorb.

This is also why I do not think eliminating duplication should be treated as a goal on its own.

Duplication is visible, which makes it easy to identify as a problem. Complexity hidden inside a highly configurable shared component can be much harder to notice.

Depending on the context, two implementations may be better kept separate, a difference may belong inside a shared component, or an abstraction that once made sense may need reconsideration as the products evolve.

What matters is that there is a reason behind the decision and that the people working on the system can still understand that reason later.

A component library only solves part of the problem

by Jakub Zerdzicki
by Jakub Zerdzicki

I encountered the same issue from a different direction in another company.

We started an initiative that was intended to evolve into a Design System. We created a shared component library and started establishing common foundations across the product.

The library was useful, and teams could reuse components instead of repeatedly implementing the same interface patterns.

But the initiative never matured as far as we originally expected.

In practice, what remained was much closer to a component library than to a complete Design System.

I think this distinction matters because people often treat the two as interchangeable.

A component library focuses primarily on implementation. It tells developers what components already exist, exposes their APIs, and gives teams a common technical foundation for building interfaces.

A Design System has to deal with a broader set of questions.

It needs to help teams understand not only what exists, but when to use a pattern, what variations are acceptable, how to handle exceptions, and how a new pattern eventually becomes part of the shared system.

It also needs ownership.

If a team needs something the system doesn't currently support, there has to be a way to decide whether to extend the existing component, keep the solution local, or eventually make the new pattern part of the shared foundation.

A library alone cannot answer those questions.

Without that layer, you can have a centralized component repository while the products built on top of it slowly diverge.

This experience changed the way I looked at Design Systems.

I became less interested in seeing them primarily as collections of components and more interested in the role they play in managing recurring decisions across a product organization.

Components, tokens, documentation, and tooling are all important parts of that. But none of them creates consistency on its own.

They provide the structure teams use to share and maintain those decisions.

Architecture is shaped by the way teams work

Foto de Kier in Sight Archives na Unsplash
Foto de Kier in Sight Archives na Unsplash

There is a part of this discussion that tends to disappear when architecture is presented through diagrams and idealized examples.

Software architecture is not created independently from the organization building the software.

A system's architecture is influenced by team size, the number of products developed in parallel, deadlines, how ownership is distributed, and how much coordination is needed to make a shared change.

When several squads work at the same time, those factors directly affect which architectural decisions are realistic.

Centralizing every decision can improve coherence, but it can also create a bottleneck if teams need to coordinate across the organization for relatively small changes. Giving teams complete autonomy removes some of that coordination cost, but over time it can cause products to diverge in unintended ways.

A team under delivery pressure will usually choose the path that lets it solve the problem in front of it.

That is not necessarily bad engineering.

It can simply be the most reasonable decision given the constraints the team has at that moment.

The problem appears when those local decisions accumulate without a mechanism for bringing the relevant ones back into the shared context.

A component created to meet a deadline stays in the codebase. Another team encounters a similar problem and builds something slightly different. A temporary exception becomes part of the normal product behavior.

Months later, someone discovers several implementations of the same idea, and nobody is completely sure whether they differ because they need to or because they evolved independently.

This is where I think consistency starts becoming an architectural problem.

The inconsistency might show up in the interface, but the reason for it can lie elsewhere.

It might come from unclear ownership, teams not knowing another solution already exists, a shared component being too difficult to extend, or documentation that explains how to use something without explaining when it should be used.

It might also be the result of a perfectly valid product decision that was never documented and is now indistinguishable from accidental divergence.

This is why adding another rule to a contribution guide rarely solves the whole problem.

Teams need enough autonomy to deliver, but the system also needs a way to bring relevant local decisions back into the shared architecture. Without that, the codebase gradually becomes the sum of all the exceptions that were never revisited.

Consistency does not mean making everything identical

Foto de Pawel Czerwinski na Unsplash
Foto de Pawel Czerwinski na Unsplash

One risk of talking about consistency this way is making it sound as though every interface should eventually converge on the same solution.

I do not think that is desirable.

The two e-commerce platforms I mentioned earlier were similar, but they were not identical. Some of those differences needed to exist, and trying to remove every one would have made the architecture less representative of the products it was supposed to support.

Over time, I became less interested in eliminating variation and more interested in understanding whether a variation was intentional.

If two components are different because they solve different problems, that can be completely healthy.

If they are different because two teams solved the same problem without knowing about each other, the situation is very different, even if the visual result happens to look almost the same.

This is also where documentation becomes more important than simply listing component properties.

Knowing that a component accepts size="small" is useful. Understanding why that size exists, which contexts require it, and whether another size should be introduced is a different kind of information.

The second kind of information preserves some of the system's reasoning.

The same applies to exceptions.

An exception does not necessarily damage consistency. Problems tend to appear when the reason for an exception is lost, and nobody knows whether it still needs to exist.

A healthy shared system should therefore support legitimate variation without losing the ability to explain it. That requires teams to understand which decisions belong to the shared system, which ones are intentionally local, and how something moves from one category to the other as the products evolve.

It also means accepting that the most reusable solution is not always the best architectural one.

Reuse is valuable when it reduces repeated work without making the shared abstraction substantially harder to understand. Once every consumer needs to understand more special cases, the cost of that reuse changes.

What the interface can tell us about the system

When we notice inconsistency in a product, we often react by fixing the visible difference.

Two buttons have different spacing. Two forms validate in different ways. Two similar flows use different interaction patterns.

From the user's perspective, those are interface problems, and sometimes correcting the interface is exactly what needs to happen.

But I also think those differences can signal something about the system producing them.

If several teams repeatedly implement the same pattern differently, perhaps the shared foundation is hard to discover or doesn't exist yet.

If teams keep creating local alternatives to an existing component, perhaps that component doesn't support the problems they are actually trying to solve.

If one component has accumulated a large number of configuration options, perhaps the abstraction is representing too many different ideas.

And if nobody knows whether two variations are intentional, what is missing may not be another component, but context around the decisions that created them.

Looking at consistency through this lens changes the question.

Instead of asking only how to make two screens more similar, we can also ask what allowed them to become different.

Sometimes the answer will still be CSS.

But often, the interesting part happened much earlier: when an abstraction was defined, when a team made a local decision, when ownership was unclear, or when the fastest solution became a permanent.

That is why I now think about consistency less as a purely visual property and more as something that emerges from a product's architecture and how the organization maintains it over time.

Component libraries, monorepos, Design Systems, clear ownership, and good documentation can all make this easier. Still, none removes the need to decide what to share, where variation is acceptable, and how those decisions should evolve as products and teams change.

The goal is not to build a system where everything looks the same.

It is to build one where the differences that do exist are understandable, intentional, and appropriate for the problems the products are trying to solve.


A note on Elira

Questions like these are part of what eventually led me to build Elira, a tool I'm exploring around understanding the structure and health of Design Systems.

A lot of Design System analysis focuses on what exists, such as components, tokens, documentation, and patterns. I'm particularly interested in what those things can tell us about the decisions behind the system and where those decisions may be becoming difficult to understand.

Back to Notes
Why consistency is an architectural problem — Notes — 55onze