Architecture

Architecture decisions your users eventually feel

Database boundaries and caching strategy sound like implementation details until they become loading states and support tickets.

There is a familiar moment in a product review where someone asks why a screen takes two seconds to appear, and the answer involves the word "join". The room usually treats this as a technical detail to be resolved offline. It is not a technical detail. It is the product, arriving late.

Every architectural decision has a user-visible consequence, and the gap between making the decision and feeling the consequence is usually measured in months. That delay is what makes these decisions so easy to make badly.

Where the boundary falls decides what can be fast

A service boundary is a promise that two things can evolve independently. It is also a guarantee that they can no longer be queried together cheaply. Draw the boundary between "order" and "customer" and you have decided, permanently, that any screen showing both will make two calls, handle two failure modes, and have an intermediate state where one arrived and the other did not.

This is worth stating plainly during design, not discovering during implementation. The question "what will this screen need to show together?" belongs in the same conversation as "what should own this data?" — and it is usually asked six weeks later.

Caching is a decision about staleness, not speed

Teams add caching to make something faster and inherit a question they did not intend to answer: how wrong is this allowed to be, and for how long? That question has a correct answer per surface, and it is a product answer.

The same cache TTL means something different on each surface
SurfaceTolerable stalenessWhat the user does about it
Marketing pageHoursNothing — they never know
Dashboard summarySeconds to a minuteRefreshes, and trusts it less next time
Account balanceNoneMakes a decision on a wrong number
PermissionsNoneSees something they should not

A single global TTL applies the marketing page’s tolerance to the balance. Nobody decides that; it is what happens when caching is treated as an infrastructure concern.

Asynchronous work is a UI contract

The moment a request is handed to a queue, the interface acquires a new obligation: to represent something that is neither done nor failed. Most products discover this obligation after shipping, which is why so many have a button that appears to do nothing.

// The queue call is the easy half.
await jobs.enqueue({ type: 'export', accountId });

// This is the part that decides whether the product feels broken:
//  - what does the button say for the next 40 seconds?
//  - what does a second click do?
//  - what happens if they navigate away and come back?
//  - how do they find out it failed, if they are no longer here?
ts

None of those four questions has a backend answer. All four are determined by a backend decision.

The practical version

  • Ask what each screen must show together before deciding what owns the data.
  • Set staleness tolerance per surface, and treat a global TTL as a smell.
  • Design the pending and failed state at the same time you decide something is asynchronous.
  • Write down which decisions are reversible. Those are the ones you can defer.

None of this makes architecture a design activity. It makes it a shared one — which is the only version that produces software people describe as feeling well made.

Working on something this applies to?