loyalty.dev

Postel’s Law - Deriving Robustness from Data in our Networks

Postel's Law: "Be conservative in what you do, liberal in what you accept from others" is a simple trope about adding robustness in disparate systems. It's also a sublime tenet in how we elicit simplicity out of this diaspora to design stable processes.

Postel's Law, also sometimes called The Robustness Principle states:

💡 Be conservative in what you do, be liberal in what you accept from others.

Jon Postel is a key contributor of the early Internet - specifically the early version TCP specification in 1980, a low-level networking protocol most communications are based on today. A key problem was the varied set of companies and protocols which were constantly evolving - How to talk to each other reliably?

arpanet.preprod.png

⭐️ "Contemporary map of the entire Internet (ARPANET) in semi-production phase. The ovals are sites/networks (some sites included more than one physical network), the rectangles are individual routers". Most hosts back then are still on the NCP protocol - the early precursor to TCP today (attribution to Wikipedia)

It’s more important that the messaging process works than conforming to the specific protocol. If different nodes in the network were strict about the inputs they received then the overall network would’ve been brittle and prone to breakdowns. Tolerating some variations would make the overall network much more robust.

Even though this for targeted for packet-switching, the same principle can be applied in various disciplines: from engineering, to design and the physical world (e.g. the power sockets you see in airplanes).

An example in Design - Form filling

A well known trope in forms UX is to minimise the number of fields asked of users - the more fields we have, the more effort we ask of the users and the lower the conversion rate.

Hence the basic rule of form design is less is more — remove all superfluous fields.

💡 i.e. being conservative in the number of fields asked of users.

However in data we get from of our customers, it’s common to have variations in what they key in. A user might fill in TX as representative of Texas for a state input. It’s the role of the system to accept these inputs and convert them to something standardised.

  • Removing whitespaces
  • Ignoring the case-sensitiveness of the input (if not relevant)
  • Applying default values where appropriate
  • Synonyms or equal values for the same purpose (e.g. the state comparison above)

💡 Here we’re being liberal in what we accept from our customers.

phone.input.ux.png

"The best practice is one where users don’t even have to think about phone number formatting or country code because it automatically displays it for them." (Attribution: UXMovement)

The key takeaway is for us to be empathetic & tolerant to the various actions or input that our UX, then standardising them to fit our use but provide still be able to give our users clear and consistent feedback.

In Practice: Robust Systems for our many Partners

For our many platforms across Flights, Hotels, Digital Cards etc, we connect to hundreds of different systems (clients or otherwise) across our ecosystem.

Systems evolve resulting in different versions of software & data formats or protocols that should continue to be compatible to each other

  • Backwards compatible: New updates that continue to accept old formats of information
  • Forwards compatible: Older versions of our software that can accept new formats

An example in our Digital Cards services

In our Digital gift cards services, we interact with plenty of suppliers, in each of these they have their own standards of the data, the format and even the way they treat outcomes (think API calls v.s webhooks).

For posterity’s sake, the diagram below shows only the formatting aspect of variations

Robustness (1).png

What about working with our many clients? It would be a mess, and more importantly, unreliable to work with us if each new protocol change resulted in an update to consuming systems trying to purchase our Amazon gift card.

The above is a simple illustration of us employing the Adapter pattern to encapsulate away the type of variation complexity our clients have to deal with. Other common patterns include the Facade for handling complicated sub-systems or downstream dependencies

A simpler code based illustration

interface GiftCard { name: string };
interface DigitalGiftCard extends GiftCard { redemptionLink: string };
interface PhysicalGiftCard extends GiftCard { delivery: 'courier', deliveryPeriod: string };

function displayName(card: GiftCard): string {
    if (!card.name) {
        return '';
      }
      return card.name.charAt(0).toUpperCase() + card.name.slice(1);
}

When we want to accept values in our parameters, we can be liberal in what we accept (to a reasonable degree of the operation you’re looking to perform).

When providing values however, we should be stricter in the type of values given (i.e. provide the DigitalGiftCard if that’s what you’re dealing with)

More techniques in code…

The aim here is for loose coupling, and 1 of the well known ones is Dependency Injection

class PaymentGateway
  def initialize(payment_gateway)
    @payment_gateway = payment_gateway
  end

  def authorize(amount)
    @payment_gateway.authorize(amount)
  end
end

class StripePaymentGateway
  def authorize(amount)
    # Process payment using Stripe API
  end
end

class AdyenPaymentGateway
  def authorize(amount)
    # Process payment using Adyen API
  end
end

# Usage
stripe_gateway = StripePaymentGateway.new
payment_processor = PaymentGateway.new(stripe_gateway)
payment_processor.authorize(100)
  • Objects do not depend on 1 another
  • PaymentGateway is now a bit more modular & reusable

An anti-pattern: Method signatures leaking out of your services

When StripePaymentGateway hard-codes a reference to currency, amount, metadata it is explicitly saying it's only willing to operate using these fields for instances of `StripePaymentGateway. The gateway refuses to collaborate with any other content unless the client class acknowledges it.

class StripePaymentGateway
  def authorize(currency, amount, metadata= {})
    # Process payment using Stripe API
  end
end

The code above exposes an attachment to static types. It is not the class of the object defining the order inputs that’s important, it’s the message you plan to send to it.

🔴 Hard coding a reference to currency, amount & metadata imposes an ordering to calling authorize

Uncoupling the dependency

class StripePaymentGateway
  def authorize(payload = {})
    currency, amount, metadadata = payload[:currency], payload[:amount], payload[:metadata]

        # Process payment here
  end
end

What's the difference here?

  • authorize lost its dependency on argument order
  • but, it gained a dependency on the names of the keys in the argument hash.

The new dependency is more stable than the old, and thus this code faces less risk of being forced to change

Also, key names in the hash furnish explicit documentation about the arguments

1 more for the road - The Observer

class Subject
  attr_reader :state

  def initialize
    @observers = []
    @state = nil
  end

  def add_observer(observer)
    @observers << observer
  end

  def remove_observer(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @observers.each { |observer| observer.update(self) }
  end

  def set_state(state)
    @state = state
    notify_observers
  end
end

class Observer
  def update(subject)
    # Do something with the updated state
  end
end

# Usage
subject = Subject.new
observer1 = Observer.new
observer2 = Observer.new
subject.add_observer(observer1)
subject.add_observer(observer2)
subject.set_state("new state")

In the above example, the Subject class is loosely coupled to the Observer class by allowing it to be added and removed dynamically through the

  • add_observer and
  • remove_observer methods.

This allows the Subject to notify any number of observers without being tightly coupled to them.

These are common patterns for the experienced practitioner and you'll see them in their various guises for most large codebases

Elevating this to the Process level

Once we have the variance simplified to a standard format, the benefits to this as an ecosystem is manifold. We can start to build reliable processes for managing our services around this

You can visualise your code or design patterns applied (adapter/ facade) acting like a funnel.

Chain a series of these services reducing these variances in client logic, what you start to get a much simpler set of datapoints (hence edge cases) to deal with.

Robustness.ChainOfProfunctors.png

You can think of this series of funnelling as a chain of profunctors. Which then allows you to build more easily additional logic to your process.

🔴 Sidetrack: What happens to development complexity when you have to introduce changes inside your core logic?

For example, our customer servicing is simplified to a 3-step follow up when an order error happens.

  • The unexpected failure is captured as a still “processing” order to the end-user
  • We try to resolve this behind the scenes with our suppliers
  • The final outcome is returned to the end user

At no point is the customer exposed to the flux that happens behind the scenes and he/ she gets a deterministic status of their order at any point in time.

Robustness (2).png

Robustness in the process starts sublime with the way we construct our platforms - generous in what we work with but conservative in the general processing done inside our system. This helps us withstand constant changes across our ecosystem (whether a 10 or 100 linked partner systems)

Postel’s Law also has it’s detractors. When do we draw the line on flexibility? If we can handle bad inputs, wouldn’t that invite bad practice on receiving whatever we can which leads to even more complexity?

This is a potential hazard where accepting poor data becomes so entrenched where we blur the lines with what is clearly bad input (e.g. Say in the extreme case of never returning any fatal errors). IETF has a segment here on “virtuous intolerance” around being clear on the intent of your process, and rejecting undefined or unusual behaviour to guard against aberrant implementations.

Incredible efficiencies can be achieved by reducing tolerance for poor data. Using the above example, we’d draw the line on working with poor content once data reaches the inner levels of our system.

💡 i.e. (Using the earlier illustration on funnelling) We wouldn’t allow external variances pollute the inner process of retrying card orders, that’d mean allowing variance to introduce complexity on our inner workings - thereby eliminating the original simplicity we had.

Information is chaotic. Errors happen always. If at every step, we take the chance to narrow the chaos, it helps to make things downstream a little simpler. In aggregate, counters to that flux, and our systems become more robust - Which then allows us to be flexible in the additional value-add we can put to our Platforms for our customers..