Planning a web application that can grow
How to scope a first release, draw clear boundaries between interface, API and data, and keep the architecture as simple as the product allows.
4 min read
Practical articles about interfaces, systems, AI workflows and software delivery. Each guide explains the reasoning behind an engineering decision.
3 guides Architecture, AI workflows, Web and UI
How to scope a first release, draw clear boundaries between interface, API and data, and keep the architecture as simple as the product allows.
4 min read
A practical structure for AI features that read documents: keep the source in view, treat uncertainty as a state, evaluate on real samples and let a person confirm what matters.
4 min read
Loading and error states, keyboard access, responsive layouts, clear feedback and performance: the details that decide whether people trust what they see.
4 min read
A web application rarely needs to be built for every future requirement on day one. It needs a first release that does one job well, boundaries that keep later changes local, and a data model that can answer the questions the business will ask. This guide walks through the decisions we make before the first screen is built.
Most web applications start as a long list of features. The list matters, but it is not a plan. A plan says which feature ships first, who it is for, what it reads and writes, and which parts of the system are allowed to depend on each other. Those few decisions do more for future growth than any choice of framework.
We define the first release as the smallest version that completes one real job for one kind of user. It is described as a set of primary journeys rather than a set of screens. A journey names the person, the steps they take and the information each step needs. For an internal approval tool, the journeys might be:
Each journey gets acceptance criteria that can be checked in a demonstration: what must be visible, what must be stored and who is allowed to do it. We also write down what is out of scope. An explicit not-yet list keeps the first release small without losing the ideas that matter later.
A growing application needs three clear boundaries: the interface people use, the API that defines what the system can do, and the data the business depends on. When those boundaries are clear, a new screen, a mobile app or an integration can be added without rewriting the parts behind it.
The rule we hold to is simple. The interface asks, the API decides and the database remembers. Validation can happen in the browser for convenience, but the decision is always made on the server, because that is the only place where a rule cannot be skipped.
An API that mirrors the database table by table pushes business rules into every client. We prefer operations that describe what a person is trying to do. Approving a request is a task with its own rules, so it gets its own operation instead of a generic update that any client could misuse.
type RequestStatus = 'draft' | 'submitted' | 'approved' | 'rejected'
interface ApprovalRequest {
id: string
title: string
status: RequestStatus
requestedBy: string
decidedBy: string | null
decidedAt: string | null
}
// POST /requests/:id/approve
// Allowed for: a manager of the requester's team
// Effect: moves a submitted request to approved and records who decided and whenContracts like this are easy to review with the whole team. The frontend engineer can build against it, the backend engineer can enforce it and the project manager can check it against the acceptance criteria. When a contract has to change, we prefer additive changes, such as a new optional field, so existing clients keep working.
Structured business data usually belongs in a relational database. We model the entities and relationships the journeys need, then let the database protect them: unique constraints, foreign keys and required fields stop inconsistent data before it is written. A few habits keep the model useful as the product grows:
A single, well-structured application with one database is often the right start. It is easier to build, test and deploy, and clear modules inside it keep the code organized. We separate a part into its own service only when there is a concrete reason: a very different load pattern, an integration that must be isolated, or a release cycle that cannot follow the rest of the product.
Slow work is the exception we plan for early. Sending emails, generating documents and synchronizing with external systems run as background jobs, so a person never waits on them and a temporary failure can be retried safely. Caching comes later, once measurements show where it is needed.
An application can only grow safely if the team can see how it behaves. From the first release we add structured logs with request identifiers, error reporting and a small set of measurements for the primary journeys. A short decision record next to the code explains why the important choices were made, so the next change starts from the reasoning rather than a guess.
None of these steps requires a large architecture. They make the first release smaller and easier to reason about, which is exactly what lets it grow.
AI features earn trust when they fit an existing workflow and make it easy for a person to stay accountable. Using document processing as the example, this guide covers source context, uncertainty, evaluation, access boundaries and the moments when a reviewer should confirm an action.
Many useful AI features do not replace a decision. They prepare it. A model can read a document and propose values in seconds, while a person remains responsible for what reaches the business system. Designing that review step well is what turns a promising model into dependable software.
Take a common example: a team receives supplier invoices as PDF files and copies the supplier name, invoice number, dates and totals into a finance system. Before choosing a model, we map the current process. Who handles each document, what they check, which mistakes are expensive and which are easy to correct.
That map decides the division of work. The AI step proposes field values. The reviewer confirms or corrects them. The application records the approved values and who approved them. Each part has one clear job, and the business system only ever receives approved data.
A reviewer cannot confirm a value they cannot check. Every extracted field therefore carries a reference to where it came from: the document, the page and the exact text the value was read from. The review screen shows the value beside its source, so checking it takes a glance instead of a search.
interface ExtractedField {
name: 'supplierName' | 'invoiceNumber' | 'issueDate' | 'totalAmount'
value: string | null
source: {
documentId: string
page: number
quote: string
} | null
state: 'ready' | 'needs-review' | 'missing'
reason: string | null
reviewedBy: string | null
}Models do not reliably know when they are wrong, so we do not rely on a single confidence number. Instead, a value is marked for review when any of several signals applies:
Each flagged value shows its reason. A reviewer who sees that the totals do not match the line items knows exactly what to check, and the team can count which reasons appear most often.
Before launch we build an evaluation set from representative documents, including the difficult ones, with the correct values agreed by the people who do the work today. The extraction step is measured field by field: correct, incorrect or missing. Those results decide which fields can be trusted, which need review every time and whether the approach is ready at all.
Evaluation continues after launch. When a prompt, a model or the mix of incoming documents changes, the same set is run again before the change is released. Reviewer corrections, where they may be kept, show where new examples should be added.
An AI step should never see more than the process it serves. It runs with the same permissions as the rest of the application, receives only the documents and fields it needs, and its requests are logged so the team can tell what was processed and when. If an external AI service is used, what it may store and for how long is part of the project's agreed requirements, not an afterthought.
We agree the review rules with the team that owns the process. A practical starting point looks like this:
The review screen deserves the same care as any other interface. It shows the document beside the fields, works fully from the keyboard, makes a correction take one step and records who approved each value. A quick, clear review keeps people engaged instead of approving out of habit.
A review step is not a sign that the AI is weak. It is the part of the design that lets a team use AI on work that matters.
People judge reliability by what an interface tells them. An application that is correct but silent while it works, or vague when it fails, still feels unreliable. This guide covers the interface details we design and check on every project.
Reliability is usually discussed as a property of servers. For the person using the software, it is also a property of the interface. They cannot see the database or the network. They see whether the screen answers their action, whether their work survives a mistake and whether the page behaves the same way every time.
Every view that loads data has more than one state, and each state needs a deliberate design. For a list of orders, that means:
Empty
Explains and offers the next action
Loading
Keeps the final layout in place
Partial
The rest of the page still works
Error
Says what happened and what to do
Ready
The data, as the person expects
Content that jumps as it loads makes people misclick and lose their place. We reserve space for images and data before they arrive, use placeholders that match the final layout and keep existing content on screen while it refreshes. Cumulative Layout Shift, one of Google's Core Web Vitals, measures this movement, and a score of 0.1 or less is considered good.
For longer operations, a precise message is better than a spinner. Saying what is happening, such as uploading three files, and showing progress when it is known tells people that the system is working on their request.
A useful error message says what happened, whether anything was lost and what to do next. Input is never cleared because a request failed. Validation errors appear next to the field they concern and are connected to it, so assistive technology reads them together.
<label for="email">Work email</label>
<input
id="email"
name="email"
type="email"
aria-invalid="true"
aria-describedby="email-error"
>
<p id="email-error">Enter an email address in the format name@company.com.</p>System errors are different from validation errors. When a save fails because a service is unavailable, the message says so plainly, keeps the person's input and offers a retry instead of blaming the form.
After an action, the interface should show its result where the person is looking: the saved row updates, the status changes or a short confirmation appears. These messages should also reach screen reader users without moving their focus. WCAG 2.2 covers this in Status Messages (4.1.3), and a polite live region, such as an element with role="status", is the usual way to meet it.
Buttons show a pending state while their request runs and cannot be submitted twice. Optimistic updates, where the screen changes before the server confirms, are used only when a failure can be reversed clearly.
An interface that only works with a mouse is unreliable for many people, and for everyone at some point. Every action must be reachable with the keyboard, in a logical order and with a clearly visible focus indicator. Native elements such as buttons, links and form fields provide much of this behavior without extra code.
Focus management matters most when the screen changes. A dialog moves focus inside when it opens and returns it to the control that opened it when it closes. Sticky headers need care too: WCAG 2.2 adds Focus Not Obscured (Minimum) (2.4.11), which requires that a focused control is not entirely hidden by content such as a fixed header.
A responsive interface is more than a layout that fits. The task must still work on a phone, on a small laptop and at 200 percent zoom. The WCAG Reflow criterion (1.4.10) asks that content can be used at a width equivalent to 320 CSS pixels without scrolling in two directions, apart from content such as data tables and maps that genuinely need a two-dimensional layout.
Touch targets need space as well. WCAG 2.2 Target Size (Minimum) (2.5.8) sets 24 by 24 CSS pixels as the baseline, with exceptions for spacing and inline links. We aim higher for primary controls, typically 44 to 48 pixels, because people often use phones on the move.
Speed is part of reliability. Google's Core Web Vitals give useful targets, assessed at the 75th percentile of real page loads: Largest Contentful Paint within 2.5 seconds, Interaction to Next Paint within 200 milliseconds and Cumulative Layout Shift of 0.1 or less.
In practice that means sizing images for where they are shown, loading only the fonts that are used, keeping heavy work off the main thread and showing a pressed or pending state immediately, even when the result takes longer.
None of these details is visible when everything goes right. They are what people notice when something goes wrong, and they decide whether the software still feels dependable.
How interfaces, APIs and data fit together, and how to keep a first release ready to grow.
1 guide published
AI features designed with source context, evaluation, access boundaries and human review.
1 guide published
Interfaces that stay clear, accessible and responsive when people rely on them every day.
1 guide published