5 min read

How much system is enough?

Qatar Airways Promotional ProjectExpedia
Engineering Essay

A booking form can carry a surprising amount of logic.

On a promotional project for Qatar Airways, I worked on a booking form connected to an Expedia booking flow. A passenger could choose a cabin class, define origin and destination, place a stopover in Doha on the way out or on the way back, and enter dates for the three flight segments.

The stopover choice changes how those segments are arranged. On the way out, the first two flights pass through Doha. On the way back, the last two do. The dates depend on each other too, so each segment has to be scheduled after the previous one.

When the user searched, the form state had to be transformed into the data structure Expedia expected.

I had built forms before, most of them much more ordinary. State management, validation, field behaviour and submission were not new problems. What changed here was how tightly they depended on the product rules around them. If the patterns kept repeating, they might deserve a system of their own.

I was not trying to turn this particular form into a system. It made me think about what might be worth standardising if the same problems kept showing up elsewhere.

Once there is a system, how much should it own?

Who owns what?

Calling something a form system does not mean the system should own everything that happens in a form.

Some of the work already belongs to the browser. Native controls come with semantics, focus behaviour, keyboard interaction and an event model. If the browser already handles those behaviours, there is little reason to recreate them ourselves.

A form engine, whether provided by a library or built internally, solves another part of the problem. It can keep track of values, touched and dirty state, errors, subscriptions and the submission lifecycle. It does not need to know what those values mean.

In the booking form, the engine could keep track of whether the passenger chose a stopover on the way out or on the way back. It did not need to understand what either choice meant for the journey.

The stopover choice changed how the three flight segments were arranged. The dates had product rules too. Flight 2 could not happen before Flight 1. The form engine could provide ways to react to field changes or compare values. But the booking logic still had to decide which dates depended on which, and what made a sequence valid.

A date field already has some reusable behaviour. It can have a known value shape, a standard control and common validation rules. Required fields, formats, ranges and similar constraints can be described consistently across many forms.

But a date being required is not the same kind of rule as Flight 2 having to happen after Flight 1.

One belongs to field and validation behaviour. The other comes from the booking model.

Different sources of truth

A form does not need one definition that describes everything.

The form structure can have one schema, and validation another. Each can be the source of truth for the thing it describes.

Not every responsibility needs to be represented as another schema. Booking-specific rules can stay in the booking model. The system receiving the result may expect a different contract again.

A date can be required because the validation schema says so. Flight 2 cannot happen before Flight 1 because of a booking rule. Both constrain the same value, but for different reasons.

The boundary shows up again when the form state is submitted.

While someone is using the form, it can make sense to keep selected locations, the stopover choice, dates and values derived for the flight segments. Expedia does not need to receive that state in the same shape.

On search, the same internal state had to be mapped to Expedia's booking format.

A reusable form system can provide a standard transformation step, while the Expedia-specific mapping stays in the integration layer.

A flight date was first a value the engine had to track. It also had field-level validation. Its relationship to the other flight dates came from the booking rules. When the form was submitted, the same date became part of the Expedia booking context.

For me, “single source of truth” became something closer to “one source of truth per responsibility, not one source of truth for everything”.

The generator question

A schema-driven generator works well when the variation between forms is predictable.

The booking form already had plenty of things a generator could describe. Origin and destination were location fields. The stopover choice was a single-choice field. Labels, whether a field was required, and common validation rules could all be expressed through reusable definitions.

The trade-off starts when the differences are no longer structural, but specific to the product.

The dates were another example. A generic system can compare values and react to dependencies, but the booking flow still has to define which dates depend on each other and what that sequence means.

What I would not want to do is turn the booking rules themselves into the vocabulary of that system. The reusable layer should provide the primitives those rules need, without trying to describe the booking rules themselves.

A product-specific schema can still build on those generic mechanisms. The abstraction starts to leak when the generic vocabulary needs booking-specific concepts just to describe the form.

If the system needs concepts such as stopover placement, journey legs or outbound and return segments, it is no longer only describing reusable form behaviour. It is starting to absorb the booking model.

A form system can still standardise the parts that genuinely repeat without requiring every form to come from the same definition. State management, field contracts, validation mechanisms, adapters and submission patterns can be shared even when the UI itself is composed explicitly.

The question is not whether more of the form can be generated. It is whether generating it actually makes the abstraction clearer and easier to use.

The runtime mechanics raise a similar question. If a lower layer already handles those mechanics well, wrapping them in another abstraction may only create another layer to maintain.

For me, the valuable boundary is the part that stays the same across products, not everything that can technically be made configurable.

Where to stop

The booking form made the boundary fairly clear. Some problems repeat across forms. Others only exist because of the product around them.

That is enough to start designing a reusable system, but not enough to justify making every future form fit the same abstraction.

A schema-driven generator may be right when the variation between forms is predictable. In other cases, it may be enough to share state management, field contracts, validation and submission while composing the UI explicitly.

The amount of system is part of the design. So is knowing where to stop.