Field note 003 / Architecture

The hidden architecture decisions inside a fast prototype

A prototype already has an architecture. It is there in who can change a record, where the rules live and what happens when an email fails. The useful job is to make those choices visible.

By Antony Doyle / / 7 min read

Imagine a small equipment-hire company replacing its booking spreadsheet. The first version is refreshingly simple: choose a drill, pick a date, enter the customer and press confirm. An email arrives. The calendar fills in. Everyone can see why this is better.

Then someone asks for a customer booking page. Another person wants a daily availability export. A neighbouring branch would like to use it too. None of these requests sounds dramatic. Together, they expose decisions that the first version never had to explain.

This is a hypothetical example, but a useful one. Whether an AI assistant generated the application or a developer wrote it in a hurry, the same questions apply. You don't need a grand redesign to answer them. You need to follow a booking through the system.

Where does a booking become a booking?

Start with the confirm button. Does its page decide whether the drill is available? Does another page contain a slightly different version of that rule? When the customer booking page arrives, will it copy both?

Give the act of making a booking one clear home. Staff screens, customer screens and imports should ask that part of the application to do the same job. It checks the rules and returns a useful outcome: booked, unavailable or missing information. The screens can still explain that outcome differently.

That boundary can be a module inside a single application. It doesn't need its own server. Martin Fowler's Monolith First explains why finding workable boundaries before separating services can reduce early complexity. It is an argument based on experience, not a guarantee that every monolith will be easy to split later.

For our hire company, keeping bookings, stock and customer records in one deployable application is a reasonable starting point. The important part is whether a developer can change the booking rule without hunting through every screen.

Let the database protect the promises

Now imagine two staff members booking the same drill for the same day. Both screens check availability before either saves. Both see a free drill. A friendly warning in the interface won't prevent the clash.

For this deliberately simple, whole-day booking model, the database could enforce one reservation per physical item per date. PostgreSQL's constraint documentation describes how a unique constraint can protect a combination of columns. Variable hire periods would need a different rule, such as preventing overlapping date ranges. Choose the rule that matches the actual business.

Application validation still matters: it provides a helpful explanation before a customer fills in a whole form. The database protection matters because another request, import or future screen can arrive by a different route. A rejected booking should become an understandable “that item has just been booked”, not a raw database error.

Also decide which record is authoritative. The availability calendar should reflect reservations; it should not become a separate, manually maintained account of the truth. If creating a booking changes several related records, decide which changes must succeed together, and put those changes in a database transaction. Concurrency protection still needs deliberate design; simply adding a transaction doesn't fix every race.

The second customer changes the question

A second branch might be part of the same business. A second hire company is different: it must never see the first company's customers, bookings or exports. That distinction belongs in the design before someone adds a company dropdown.

Work out whether accounts belong to a company, whether people can work for several companies and who can invite colleagues. Then enforce that membership when records are read or changed. A company identifier supplied by the browser is a request to check, not proof of permission.

The boundary needs to follow the data. It applies to search, downloadable files, background jobs and support access as well as the main booking page. A global administrator role also needs a clear purpose; “we might need it” is a poor reason to hand out access to everybody's records.

Some databases offer an additional enforcement layer. PostgreSQL's row security policies can restrict which rows a database role may read or change. They need careful configuration: table owners normally bypass these policies, as do superusers and roles with the relevant bypass privilege. Test using the application's real database role, including attempts to access another company's data.

What happens after the click?

The booking is saved, but the email provider is unavailable. Has the booking failed? For this application, probably not. The drill is reserved; the confirmation still needs sending. Those are two separate facts, and the interface should say so.

A practical design is to record the booking and the need to send its confirmation together, then have a worker process pending messages. That gives the application something durable to return to after a restart. A promise to “send it later” held only in memory disappears when the process stops.

Retries introduce another question. What if the provider accepted the email but its response never reached us? Sending again may produce a duplicate. AWS's guide to idempotent APIs explains how request identifiers help a service recognise repeated attempts. Whether a particular email provider supports that contract must be checked; an internal job identifier alone cannot guarantee one delivery.

For a booking confirmation, an occasional duplicate might be tolerable. A duplicate charge would deserve a different response. Name the consequence, give failed work an owner and make it possible to see what is waiting. The queue technology is the smaller decision.

A release changes more than the code

Suppose the next version replaces a single hire date with a start and end date. Deploying new code is only part of that change. Existing bookings must retain their meaning, and the old version may still run briefly while the new one starts.

Plan an order that keeps both versions able to read and write compatible data during the transition. That might mean adding the new fields, temporarily maintaining both representations, populating existing records and checking that new writes stay in step before switching readers. Keep the old representation current for as long as rollback needs it. Remove it in a later release, after retiring the versions that depend on it. The exact sequence depends on the database and deployment process.

Likewise, decide where uploaded documents live and which process runs scheduled work. A file on one application server may not be available on another. Starting two copies of the application may also start two copies of its nightly job. These are ordinary consequences of a hosting arrangement, and they are much easier to address when they are visible.

Leave the next person the reason

You don't need a document describing every folder. Write down the decisions someone might reasonably question later: why bookings and stock share a database, how companies are separated, what counts as a confirmed booking and which email failures are acceptable.

Michael Nygard's original architecture decision record proposal gives these notes a small, useful structure: context, decision, status and consequences. Keep superseded decisions so the reasoning survives a change of direction.

A decision worth keepingFor the initial hire system, one application owns bookings and stock. This keeps reservation changes together and suits the small team. Email runs separately because its failure must not undo a booking. Revisit the arrangement if separate teams need independent releases or measured workloads create a specific problem.

That paragraph leaves room to grow without pretending the future is settled. It also gives the next developer, or the next AI-assisted change, something more useful than a folder layout to follow.

A fast prototype can be a very good beginning. Before adding its next feature, follow one important action from screen to storage to background work. Find the choices it already makes. Keep the sensible ones, correct the risky ones and leave a short explanation. That is often enough architecture work to make the next step much easier.

Further reading

For the wider release decision, read Your AI-built app works. But is it ready for production?

Want a second pair of eyes?An AI-generated code audit can make these decisions visible in your own application. See Siege21's engineering services for help working through the changes.