Back

Inside MOST Tech: Type-Driven Design in Action

Inside MOST Tech: Type-Driven Design in Action

At MOST, reliability starts long before a transaction happens. Every inflight purchase depends on software that’s designed to get things right, every time, even offline. That’s why our engineering team invests heavily in architectural patterns that reduce risk and build confidence from the ground up.

For airlines that reliability translates into fewer transaction errors, faster certification, and safer offline operations. all critical when millions of payments are processed in the air or on the ground.

One of those patterns is Type-Driven Design, an approach that allows us to encode business rules directly into our codebase, catching entire classes of bugs before they ever make it to testing or production. In complex environments like airline retail and payment processing, where precision and compliance are non-negotiable, this approach makes a measurable difference.

In this article, one of our senior developer walks through how type-driven design helps prevent common logic and data errors in systems that handle everything from flight operations to sensitive payment data.

Reducing bugs via Type Driven Design

Inside MOST Tech article by Lovro Buničić

Where standard tools fall short

Every product is built around a set of business concepts and rules that make it unique. These rules can be complex and constantly evolving, which makes correctness difficult to maintain over time. While tools and testing frameworks help, most don’t understand the specific domain logic that drives our product.

Languages with strong type systems already eliminate many classes of bugs automatically. You can’t divide two words or add a string to an integer because the compiler prevents it. Type-driven design takes this one step further: instead of only relying on language-level types, we model our own domain types to capture business meaning directly in the code.


In fintech and specially with airline systems, small logic errors can have wide ripple effects, from mis-tagged flights to reconciliation delays. This is where standard tools reach their limits.

By designing precise custom types, we reduce the need for defensive programming. We guide developers toward correct usage by making valid paths obvious and invalid ones impossible.

Example 1: Avoiding mixed IDs

Image

In aviation retail systems, different entities such as flights, containers, and flight legs are often identified by similar-looking IDs. These identifiers might all be strings or UUIDs, making them easy to mix up accidentally, a drawer ID used where a flight ID was expected, for example.

When related concepts are modelled with similar types, it becomes ambiguous which specific ID the API expects in a given context. Developers are then forced to rely on documentation or delve into implementation details to determine the correct identifier - an inefficient and error-prone process.

For example, when packing a drawer, multiple drawer IDs may be available, but without clear type distinctions, it is not immediately obvious which one should be used.

Image

By encoding the meaning and distinction between the IDs into separate types, we constrain the usage of our API and disallow misuse.

Image

When packing a drawer, the correct usage becomes immediately clear and misuse is virtually impossible. The compiler will catch any errors at an early stage, long before the code has a chance to reach production.


By turning FlightId, DrawerId, and LegId into distinct types, the compiler blocks misuse up front, no special docs or tribal knowledge required.

Image


The intention is clear. To make this a bit easier, we can construct a phantom type ID.
Phantom types are generic types that don’t use the generic property. The generic parts are only to distinguish between the different types.

Image
Image

We can add some syntax sugar to make the distinction between the IDs even more pronounced.

Image

If the ID is indeed meant to be shared between the models, we can compose an ID constraint referencing the different models.

Image

Example 2. PAN

Image



Another one of the highly complex industries is the payment card industry. The system must delete references to the user's card information as soon as it's no longer needed. And while in the system, it may be used only for the narrow functionality of taking a payment, and doing so in a secure manner.


In an airline context, this can mean the difference between a smooth onboard transaction and a compliance breach. Manual controls are not enough, the system itself must enforce safe handling.

One of the most sensitive pieces of information is the PAN, but it is just a String. So, having to remember to validate a pan, not to log it anywhere, not to display it in the UI unmasked, and so on. The system may even have modules that only allow for masked PAN or truncated PAN. So, whenever a change or feature is implemented that affects any of those aspects, the developer bears the responsibility for ensuring safety, followed by the reviewer and then the tester.

Image

By applying the concepts of type-driven design, step by step, we’ll create a solution that solves the majority of the mentioned concerns.


In onboard payments, these controls translate directly into fewer compliance breaches, faster audits, and safer integrations with acquirers.

Image



1. Validating
Instead of PAN being a String, it should be - PAN. Instead of validating the String through the system, we instead parse the data as soon as we acquire it, so whenever there is a PAN, we can trust its validity.

Image


2. Ensuring safety
Instead of having to check if the PAN is masked or truncated before performing sensitive actions, we encode different levels of PAN security in the type itself. So, by using some sort of union type, we add SensitivePan, MaskedPan and TruncatedPan. Each of those provides getters and methods safe and appropriate for a given state. E.g. possibility of logging, storage, or display should be different for different states. Also, some PANs should not reach certain modules of the system.


SensitivePan can be masked and truncated.

Image

Image
Image

Now, alongside being sure PAN is validated, we can also be sure it's safe to use in a given context. Certain actions now only receive appropriate pan, making it obvious which pan to provide, and making it very hard to misuse.

Image

3. Consuming it

After we have used up our sensitive payment information, the developers need to ensure that it is removed from the system. But simply creating a truncated version won’t prevent us from using or storing the sensitive one. Some languages provide options for consumable types (e.g., consumable methods in Swift); in others, we can achieve similar effects by adding a consumable type.

Image

Ultimately, we cannot prevent returned String from being copied or stored, but we are limiting the API surface and making it obvious. Since SensitivePan will only be consumed inside a payment method, misuse can now only happen in this isolated (and usually small) environment.

Image

Final Thoughts

Well-designed types do more than prevent bugs, they can eliminate entire classes of tests. For example, if your types make invalid states unrepresentable, you don’t need to write tests for those cases.

Types also serve as live documentation, reducing reliance on external documentation that can quickly become outdated.

This becomes especially important when you are consuming an SDK or library where you don’t have access to the implementation details. As a consumer, you can’t simply peek under the hood to figure out how things work or why something fails. You don’t need to reach out to the SDK provider for clarification or to debug subtle issues, the type system itself makes the contract self-explanatory.


For airline and finance teams, the benefits are straightforward: fewer operational incidents from logic errors, shorter QA cycles before deployment, and more predictable upgrades that respect data-safety rules by design.


Robust type systems also make large-scale refactoring safer and more predictable. When types encode your business rules, the compiler guides you through changes and highlights places that need attention.

Adopting type-driven design doesn’t have to be all-or-nothing. You can start small, focusing on the most error-prone or business-critical areas first. Take advantage of features your language already provides—like enums with associated types, sealed classes for modeling choices, or consuming methods in Swift.

By embedding our business constraints into the type system, we are able to free the developers from the tedious need to repeatedly enforce and test domain rules and constraints, allowing them to focus their energy on delivering real product value. While automatic and manual testing are still a crucial part of any development workflow, a certain class of bugs is eliminated before they can be included in the codebase.

Tags: