Pipeline Pattern

You don't need this pattern. But sometimes it makes everything cleaner. Lets first review various scenarios we all have seen, that could take advantage of a different way to handle things.

Order checkout: Validate cart → Reserve inventory → Charge payment → Create order record → Send confirmation.

Each step has a dependency on the previous one. Payment can't happen without reserved inventory. The order record needs a successful charge. If something fails mid-flow, each step knows how to undo itself. Payment failed? Release the inventory. Order creation failed? Refund and release.

Email validation: Parse format → Check domain blacklist → Verify business domain → Check for disposable email providers → Validate MX records exist.

Same idea, Each step builds on the last. No point checking if it's a business domain if the format is garbage. No point hitting DNS for MX records if the domain is blacklisted. Fail fast, fail early.

Could you build either of these without a pipeline? Sure of course. Nest some try/catch blocks, sprinkle in some cleanup logic. It'll work until you need to add a step. Or remove one. Or debug why inventory got reserved but payment never ran. Or you could use the Saga pattern and these concerns are voided. But +23923 complexity.


Damn Jake, But What Is it?!

A pipeline is a sequence of steps where each step receives data, transforms it, and passes the result to the next step. You know, a pipeline!!! The context flows forward. If any step fails, you walk backward through completed steps and run their rollback logic.

That's it. Function composition with built-in undo.

The pipeline pattern makes the flow explicit. Each step is isolated. Rollbacks are defined where they belong. Adding fraud detection between payment and order creation? Slot it in. Need to skip compression for certain file types? Swap that step conditionally.

It's not magic. It's just structure that pays off when the sequence matters.

Here's an attempted code sample of everything combined BUT each step is just a validator with context, no defined rollback.

interface EmailValidationContext {
  email: string;
  domain: string;
  isValidFormat: boolean;
  isBlacklisted: boolean;
  isBusinessEmail: boolean;
  isDisposable: boolean;
  hasMxRecord: boolean;
}

const BLACKLISTED_DOMAINS = new Set(["spam.com", "fake.net", "blocked.org"]);
const DISPOSABLE_PROVIDERS = new Set(["tempmail.com", "throwaway.io", "10minute.email"]);
const PERSONAL_DOMAINS = new Set(["gmail.com", "yahoo.com", "hotmail.com", "outlook.com"]);

type ValidationStep = (ctx: EmailValidationContext) => EmailValidationContext;

const parseFormat: ValidationStep = (ctx) => {
  const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  if (!pattern.test(ctx.email)) {
    throw new Error("Invalid email format");
  }
  ctx.isValidFormat = true;
  ctx.domain = ctx.email.split("@")[1].toLowerCase();
  return ctx;
};

const checkBlacklist: ValidationStep = (ctx) => {
  if (BLACKLISTED_DOMAINS.has(ctx.domain)) {
    ctx.isBlacklisted = true;
    throw new Error(`Domain ${ctx.domain} is blacklisted`);
  }
  return ctx;
};

const checkBusinessEmail: ValidationStep = (ctx) => {
  ctx.isBusinessEmail = !PERSONAL_DOMAINS.has(ctx.domain);
  return ctx;
};

const checkDisposable: ValidationStep = (ctx) => {
  if (DISPOSABLE_PROVIDERS.has(ctx.domain)) {
    ctx.isDisposable = true;
    throw new Error("Disposable email addresses not allowed");
  }
  return ctx;
};

const validateMxRecord: ValidationStep = (ctx) => {
  // Yah, I just wanted an example for this step but I'm not writing an actual way to validate this for an example hahaha.
  ctx.hasMxRecord = true;
  return ctx;
};

function validateEmail(email: string): EmailValidationContext {
  let ctx: EmailValidationContext = {
    email,
    domain: "",
    isValidFormat: false,
    isBlacklisted: false,
    isBusinessEmail: false,
    isDisposable: false,
    hasMxRecord: false,
  };

  const steps: ValidationStep[] = [
    parseFormat,
    checkBlacklist,
    checkBusinessEmail,
    checkDisposable,
    validateMxRecord,
  ];

  for (const step of steps) {
    ctx = step(ctx);
  }

  return ctx;
}