A frontend architecture you can still understand in two years
Most frontends do not become hard to work with overnight. They get there gradually, one reasonable-seeming decision at a time, until one day a small change requires touching a dozen files and nobody is quite sure what will break. We build a lot of long-lived web applications, and over the years we have settled on a handful of principles that keep them understandable well after the excitement of the first launch has faded.
Organise by feature, not by file type
The single most useful decision we make is to group code by what it does, not by what kind of file it is. A folder named invoicing that contains the components, state, and helpers for invoicing is far easier to reason about than three parallel folders called components, hooks, and utils with the invoicing logic scattered across all of them. When a feature lives in one place, you can understand it, change it, and — crucially — delete it without an archaeological dig.
This sounds obvious, but the file-type layout is seductive because it looks tidy at the start, when there are only a few files. The feature-based layout pays off later, exactly when it matters most.
Draw clear boundaries and respect them
Within an application we try to keep a clear line between three kinds of code: the parts that talk to the outside world (loading and saving data), the parts that hold business logic, and the parts that render the interface. When these get tangled — when a button component knows the shape of an API response, for instance — every change ripples further than it should.
We keep data-fetching at the edges and pass plain, well-shaped data inwards. Components receive what they need and render it; they do not reach out and fetch things themselves. This makes components easy to test, easy to reuse, and easy to reason about, because their behaviour depends only on what you hand them.
A component should be boring. The interesting decisions belong in clearly named functions that you can read and test on their own.
Name things for the next person
Naming is not a cosmetic concern; it is how the next person navigates the code. We spend real effort on it. A function called calculateLateFee tells you what it does; a function called process tells you nothing and invites a kitchen-sink of unrelated logic. When we find ourselves struggling to name something, we treat that as a signal that the thing is doing too much, and we split it.
Resist premature abstraction
The most expensive mistakes we see are not duplicated code; they are the wrong abstractions built too early. Two pieces of code that look similar today may need to change in completely different directions tomorrow. When you force them to share an abstraction prematurely, every future change has to fight that shared code. We have a rule of thumb: wait until we have seen the same pattern three times before we extract it. A little duplication is cheaper than the wrong abstraction.
Make state changes explicit
State is where frontends get confusing. We keep as little of it as we can, we keep it close to where it is used, and we reach for global state only when several distant parts of the application genuinely need to share it. When state does change, we want those changes to happen in obvious, named places rather than as side effects buried deep in rendering logic. A reader should be able to answer "what can change this value, and when?" without tracing through the whole codebase.
Invest in the seams that tests need
Code that is easy to test is usually code that is well structured, because testing forces you to separate logic from its surroundings. We do not chase a coverage number, but we make sure the important behaviour — the calculations, the rules, the edge cases — lives in functions that can be tested directly, without spinning up the entire interface. When the logic is testable, the components on top of it can stay thin.
Keep dependencies few and boring
Every dependency is a small bet that someone else will keep maintaining it, keep it secure, and not break it in a future version. We make those bets sparingly. For anything central to the application, we prefer well-established libraries with a track record, and we are happy to write a little code ourselves rather than pull in a package to save a few lines. Fewer dependencies means fewer surprises at upgrade time, and upgrade time always comes.
The thread that ties it together
None of these ideas is clever in isolation. What they have in common is a bias toward code that a reasonably experienced developer can pick up, understand, and change safely — including the version of ourselves who has forgotten all the context two years from now. That future reader is the real customer for an architecture, and designing for them is the most reliable way we know to keep a frontend healthy over the long run.
Written by the Arcwell engineering team. If you're wrestling with something similar, we're happy to compare notes.