When organizing component structures, it’s easy to fall into the trap of the
"lazy" approach: throwing everything into a single ui/ folder. The problem is
that this usually leads to a mess once the application actually starts growing.
I prefer splitting component layers into three levels: primitives, patterns, and domain. Deciding where a component belongs is an art; if you get it wrong, reusing those components in another project becomes a headache.
First are the primitives. These are the building blocks like Button,
Input, or Checkbox. They don't know anything about your business logic.
Their only job is aesthetics, accessibility, and basic interaction behavior.
Then you have patterns. These are how we arrange primitives to perform
specific tasks, for example, a FormField that bundles a label, an input, and
an error message. Patterns are the choreography between components, but they
still don't carry any "business names."
Finally, there is the domain. This is where components start to have actual
meaning within the product, like InvoiceStatusBadge or ProjectPicker. These
components use the product's vocabulary; they actually know what an "invoice" or
a "project" is.
A common mistake is mixing these three up. I often see components inside a
design system package that suddenly start fetching data or have props like
entity="invoice". Once a component starts calling a query hook or holding API
logic, it has crossed into domain territory and is no longer just ui.
When should a component go into a shared UI package? Only if it is product-agnostic. This means if you were to build a completely different app tomorrow, that component would still work without needing a rename.
On the other hand, keep components within the application code if they reference business entities, check permissions, or follow a workflow specific to one feature. Extracting components too early often just becomes an unnecessary cost.
The patterns layer is also easily misused. Many people create a patterns/
folder that ends up becoming a "junk drawer" for half-baked components that have
accidentally leaked in some business logic. Remember: patterns can manage
accessibility, but they shouldn't know about your API error codes.
Maintaining these boundaries is exhausting at first, but staying disciplined early on is vital. The goal is to keep your design system lean so the whole system doesn't turn into an uncontrollable giant.
That's the shape of it for me.