Skip to content
Blog

Engineering notes from UNISON

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

Featured guide

Architecture

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

Read the articles in full

Planning a web application that can grow

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.

Start with the first useful release

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:

  • A team member submits a request with the details a manager needs.
  • The manager reviews the request, approves or rejects it and adds a comment.
  • The team member sees the decision and the history of the request.

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.

Draw the boundaries before the screens

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.

Web interfaceScreens, forms and convenience checks
Authenticated requests
APIPermissions, validation and business rules
Reads and writes
DatabaseConstraints and migrations
Queues slow work
Background jobsEmails, documents, sync
Calls out
IntegrationsExternal services
Diagram of a web application. The web interface sends authenticated requests to an API. The API applies permissions and business rules, and reads and writes a relational database. A background worker handles slow tasks, and integrations connect to external services. An example structure for a first release. The interface never talks to the database directly: every action passes through the API, where permissions and business rules live.

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.

Design the API around tasks

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.

Request model and approval contract
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 when

Contracts 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.

Model the data you need to answer questions

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:

  • Record who created or changed important records, and when.
  • Keep status changes as events when the history matters, not only the latest value.
  • Store derived values only when there is a measured reason, and document how they are recalculated.
  • Change the schema through versioned migrations, never by hand.

Where simple architecture is enough

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.

Make growth visible

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.

A planning checklist

  • Primary journeys are written down with acceptance criteria.
  • An out-of-scope list exists and has been agreed.
  • Interface, API and data responsibilities are separated.
  • Every protected action is checked on the server.
  • The data model has constraints and versioned migrations.
  • Slow or unreliable work runs in background jobs.
  • Logs, error reporting and key measurements are in place before launch.

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.

Building AI workflows with a human review step

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.

Start from the workflow, not the model

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.

  1. DocumentIncoming PDF
  2. ExtractionValues with sources
  3. ChecksReady or needs review
  4. ReviewerConfirms or corrects
  5. SystemApproved values only
Flow diagram. An incoming document goes to an extraction step that proposes field values with source references. Values that pass every check are marked ready and uncertain values are marked for review. A reviewer confirms or corrects them before the approved values are written to the business system. The AI step proposes, the reviewer decides and the system records. Nothing reaches the business system without an approval.

Keep the source attached to every value

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.

An extracted field with its source and review state
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
}

Treat uncertainty as a state

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:

  • The field is missing, or its source text cannot be found in the document.
  • The value fails a rule, such as a date in the wrong format or line items that do not add up to the total.
  • The value disagrees with existing records, such as an unknown supplier.
  • The document itself is unusual: a poor scan, an unexpected layout or several invoices in one file.

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.

Evaluate before and after launch

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.

Respect access boundaries

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.

Decide when a person must confirm

We agree the review rules with the team that owns the process. A practical starting point looks like this:

  • Always confirm before an action with an external effect, such as a payment, a message or a deletion.
  • Always confirm values marked for review, with the reason shown.
  • Accept values without review only for low-risk fields, only when evaluation supports it, and keep that acceptance reversible and recorded.

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 short checklist

  • The workflow is mapped before a model is chosen.
  • Every value keeps a reference to its source.
  • Uncertain values are flagged with a reason.
  • An evaluation set exists and is run again after every change.
  • The AI step follows the application's permissions.
  • Actions with external effects always need a person's confirmation.

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.

Making an interface feel reliable

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.

Design every state, not only the finished one

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: no orders yet, with an explanation and the next useful action.
  • Loading: the layout is in place while the data arrives.
  • Partial: one panel failed while the rest of the page stays usable.
  • Error: what went wrong, what is safe and what to do next.
  • Ready: the data, sorted and filtered the way the person expects.
  • 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

Five small interface sketches of the same order list: empty with a next action, loading with placeholders, partially loaded with one failed panel, an error with a retry action, and ready with rows of data. One view, five states. Designing them together keeps the layout stable and the messages consistent.

Loading that keeps its shape

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.

Errors people can act on

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.

A form field connected to its error message
<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.

Feedback that confirms what changed

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.

Keyboard access is a reliability feature

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.

Responsive means the task still works

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.

Performance people can feel

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.

A review checklist

  • Empty, loading, partial, error and ready states are designed for every data view.
  • Layouts reserve space for content that loads later.
  • Error messages explain what happened and keep the person's input.
  • Results and status messages are visible and announced.
  • Every action works with the keyboard and has a visible focus indicator.
  • The interface works at 320 CSS pixels wide and at 200 percent zoom.
  • Core Web Vitals are measured against the targets above.

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.

Topics

What you can expect to learn

Each guide explains the reasoning behind an engineering choice with concrete examples, so it stays useful beyond a single project.
  • Architecture

    How interfaces, APIs and data fit together, and how to keep a first release ready to grow.

    1 guide published

  • AI workflows

    AI features designed with source context, evaluation, access boundaries and human review.

    1 guide published

  • Web and UI

    Interfaces that stay clear, accessible and responsive when people rely on them every day.

    1 guide published

Related capabilities

From notes to working software

The guides describe how we approach interfaces, systems and AI. The Capabilities page shows what the team builds with them.
Made with Modulify