Category: Custom Software

  • Event-Driven Architecture: When the Complexity Actually Pays Off

    Abstract circuit board pattern symbolizing event-driven systems
    Photo by Alexandre Debieve on Unsplash

    Event-driven architecture has been the default recommendation in conference talks for so long that most teams reach for Kafka before they have asked whether they need a broker at all. The result is a generation of CRUD applications carrying the operational weight of a distributed log they will never use to its potential. The honest framing is that event-driven is a powerful pattern with a high fixed cost, and the question is whether your workload pays that cost back.

    This post is a decision tool. It assumes you understand the basic mechanics of Kafka, NATS, Pulsar, and equivalent systems. What it gives you is a way to tell whether your workload is one of the ones where EDA earns its keep, or one where you have just bought yourself a second on-call rotation.

    What Event-Driven Actually Buys You

    Strip away the slideware and the real benefits of an event broker between services come down to four things. Producers and consumers decouple in time, so a slow downstream cannot stall an upstream. Multiple consumers can read the same stream independently, which makes fan-out and replay possible. The log itself becomes an audit trail you can rewind. And throughput tends to scale linearly with partitions in a way that point-to-point HTTP rarely does.

    Each of these benefits has a counterpart cost. Decoupling in time means you have to design for eventual consistency. Independent consumers mean schema governance becomes a real discipline rather than a per-call concern. The audit trail is only useful if you can replay it without breaking idempotency. And linear throughput requires you to actually run the brokers at scale, which is its own engineering project.

    The Real Triggers for EDA

    Three signals reliably indicate that an event-driven pattern will pay back its cost. When your workload checks all three, EDA is the right call. When it checks one, you might be better served by a queue. When it checks none, you are looking for a problem to fit a tool.

    Three or More Producers Writing to the Same Logical Stream

    If a single user action produces an event that ten downstream systems need to react to, point-to-point integration becomes an N-times-M problem within a year. Order placement is the canonical example. The order goes to fulfillment, accounting, fraud, the loyalty program, the recommendation engine, the notification service, the analytics warehouse, the search index, the customer-facing dashboard, and the partner API. Building 10 HTTP integrations works. Building the 11th breaks down. A broker turns this into a single publish and 10 subscriptions.

    Asynchronous Workflows With Long Tails

    If the work that follows a user action takes more than a few hundred milliseconds, you are already in async territory. Document processing, ML inference pipelines, batch reconciliation, video transcoding, and large-scale notifications all benefit from a broker because the producer should not block on the consumer. The alternative, a synchronous fan-out across multiple HTTP services, creates cascading failure modes that are hard to test and worse to debug at 3am.

    Audit Trail or Replay as a First-Class Requiremen
    Macro photo of a green printed circuit board with chips and traces
    Photo by Umberto on Unsplash
    t

    If regulators, compliance teams, or product analysts need to reconstruct what happened in your system at a point in time, the log itself becomes the system of record for those events. Financial services, healthcare claims, fraud investigations, and any workflow where state transitions are reviewable months later all benefit from this property. A database with audit triggers can fake it for a while. A real event log scales further and supports replaying events into new consumers without rebuilding history from scratch.

    The CRUD Anti-Pattern

    The most common misuse of EDA is putting Kafka in front of an internal admin tool that updates a table and emits a webhook. The producer is one service. The consumer is one service. The throughput is fewer than ten events per second. The audit requirement is satisfied by the database itself. There is no replay scenario beyond “check what happened yesterday” which a SELECT covers in 50 milliseconds.

    What you have built in that scenario is an HTTP call with extra steps and a new failure mode. The broker can be down. The schema can drift. The consumer can fall behind. None of these problems existed before, and you will spend the next year building tooling to make them visible. The honest design for that workload is a synchronous call, or a database trigger if you must, or at most a managed queue like SQS or Cloud Tasks. Save Kafka for when the shape of the workload actually demands it.

    The Operational Cost Nobody Models Up Front

    The reason EDA decisions go badly is that the operational cost is invisible during the architecture review and overwhelming during the second year of production. A realistic cost model includes the items below.

    • Broker operations: Kafka self-hosted needs a dedicated platform team, ZooKeeper or KRaft expertise, capacity planning, partition rebalancing under load, and an upgrade cadence that touches every consumer. Confluent Cloud, MSK, NATS Cloud, and StreamNative remove the worst of this but introduce a five to seven figure annual line item.
    • Schema governance: Avro or Protobuf with a schema registry is not optional past three producers. Without it, every consumer breaks every time a producer adds a field. With it, you have a new system to operate, version, and migrate.
    • Idempotency and exactly-once semantics: at-least-once is the default. Building consumers that are safe to replay is a nontrivial discipline that touches every team. Most teams underestimate this and ship duplicate-side-effect bugs for years.
    • Replay tooling: the ability to reset offsets and replay a topic into a consumer sounds simple until you discover the consumer cannot tell the difference between a replay and a real event. Solving this requires deduplication keys, idempotency tables, or content-addressed events.
    • Observability: tracing across an event boundary breaks most APM tools unless you propagate context manually. End-to-end latency becomes harder to measure. Lag monitoring becomes its own dashboard.
    • Disaster recovery: cross-region replication for Kafka is a real engineering project. The default assumption that the broker is durable does not survive the first regional outage.

    A reasonable rule of thumb is that the fully loaded cost of running a production-grade Kafka or Pulsar deployment with the surrounding to

    Abstract long exposure light trails evoking event streams
    Photo by Joshua Sortino on Unsplash
    oling is at least one senior engineer’s time on a continuing basis. Below that level of investment, you do not have an event-driven platform. You have a single point of failure that nobody understands.

    Choosing the Broker

    Once you have decided EDA is justified, the broker choice is mostly about operational fit. Kafka remains the default for high-throughput log-based workloads, with Confluent Cloud or MSK removing most of the operational burden at a real cost. NATS JetStream is the right choice for low-latency request-reply and lightweight pub-sub, particularly when you need a footprint that runs comfortably on a single node and grows from there. Pulsar wins when you need true multi-tenancy, geo-replication, and tiered storage out of the box, and lose-out where the operator ecosystem is thinner. Redpanda is a credible Kafka-compatible alternative when you want lower latency and simpler operations on the same client APIs. Cloud-native managed queues like SQS, Cloud Tasks, and Service Bus are not event logs but cover most async workflow needs without the operational tax.

    The Migration Path That Actually Works

    The teams that succeed with EDA almost never start there. They migrate into it deliberately, one workflow at a time, after a synchronous version has revealed the actual integration shape. The pattern looks like this: build the first version with HTTP calls and a managed queue (SQS, Cloud Tasks, or Service Bus) for any async work. Operate it for at least six months. When you find yourself adding the third or fourth consumer to the same business event, that is the signal to introduce a real broker for that specific stream. Not for the whole system. Not as a platform initiative. For the one workflow that has earned it.

    The opposite path, building on Kafka from day one because the architecture deck said so, fails predictably. The team spends the first quarter on broker operations instead of product. The schemas are wrong because the workflows have not stabilized. The consumers ship duplicate-handling bugs because they were written before anyone understood the real failure modes. By the time the platform is mature, half the original use cases have changed and the broker is in the wrong shape for the actual business.

    When This Pattern Applies

    EDA pays off when you have multiple independent consumers of the same business event, when async workflows dominate the user experience, when audit and replay are first-class requirements, and when you have the operational maturity to run brokers at production grade. Order processing, financial transactions, IoT ingestion, ML feature pipelines, change data capture, and large-scale notification systems are the natural fits.

    When It Does Not

    EDA does not pay off for internal CRUD tools, for two-service integrations where a synchronous call works, for low-throughput workflows where a managed queue covers the async case, or for teams without the headcount to own broker operations. It does not pay off when the team is still learning distributed systems and would benefit from a year of monolith discipline first. And it does not pay off when the only argument for it is that a vendor or a conference talk said it was best practice. Best practice is whatever survives your post-mortems.

  • Build vs Buy: A Decision Framework for Custom Software vs SaaS

    Team collaborating around a whiteboard during a strategy session
    Photo by Mapbox on Unsplash

    Every quarter, an engineering org somewhere greenlights a custom build that should have been a SaaS subscription, or signs a SaaS contract for the one capability that defines its product. Both mistakes cost millions. The question is not whether to build or buy. The question is which decision rule survives contact with the next five years of your roadmap.

    This framework is the one we walk CTOs through during architecture reviews. It assumes you already know how to read an invoice and how to estimate a sprint. What it gives you is a way to defend the decision to a board, a CFO, and to your future self when the tradeoffs surface eighteen months in.

    The Differentiator Rule

    The first filter is brutal and binary. If a capability is part of how you win in the market, build it. If it is not, buy it. Auth flows, billing, helpdesk, error tracking, feature flags, internal analytics dashboards, document signing, video conferencing, status pages: these are not where you win. Customers do not pay you because your SSO is elegant. They pay you because of the thing your competitors cannot do.

    The trap is that engineering teams genuinely enjoy building these things. They are well-scoped, satisfying problems with clear shapes. Auth0, Stripe, Zendesk, Sentry, LaunchDarkly, DocuSign, Daily, Statuspage, Mixpanel, Segment, Snowflake, Looker, Datadog all exist because thousands of teams concluded that those problems were solved well enough by people who solve them full-time. Your team should reach the same conclusion before they write the first migration.

    Total Cost of Ownership Beyond License Fees

    License fees are the most visible cost and almost never the largest one. A useful TCO model spans five years and counts every line item that engineering, finance, and security will eventually pay. The numbers below are illustrative bands we have seen across mid-market consulting engagements, not benchmarks.

    • Build path: initial engineering (loaded cost per FTE multiplied by team size and duration), opportunity cost of those engineers not shipping product features, ongoing maintenance at roughly 15 to 25 percent of initial build per year, on-call burden, security patching, dependency upgrades, infrastructure spend, observability, compliance audits when in scope, and the eventual rewrite that arrives every 4 to 7 years.
    • Buy path: contract value, integration engineering for connecting the SaaS to your stack, vendor management overhead, data egress costs, audit and procurement effort, and the cost of switching if the vendor underperforms or repackages pricing.
    • Hidden cost on both sides: the time leadership spends defending the decision when something breaks. Build it and an outage is your fault. Buy it and an outage is the vendor’s fault but still your problem.

    The honest version of TCO almost always shows that buying is cheaper for the first 18 to 36 months and that build economics only start to compete once your usage scale outgrows the vendor’s pricing model. Below 10,000 active users, build is rarely cheaper. Above 1 million, the math sometimes flips, but not always.

    Switching Cost as the Real Lock-In

    <
    Two paths diverging in a minimalist landscape representing a build or buy decision
    Photo by Vladislav Babienko on Unsplash
    !– /wp:heading –>

    The standard concern about SaaS is vendor lock-in. The standard concern is misframed. The real question is switching cost, and switching cost applies equally to your custom build. A homegrown billing system is locked in too. The lock-in is just to your own team’s tribal knowledge instead of to a vendor’s roadmap.

    What Increases SaaS Switching Cost

    Proprietary data formats with no clean export, deep workflow integrations with custom logic, identity provider entanglement, vendor-specific UI embedded in your own product, and pricing models that compound with usage so that migration windows become exorbitant. The mitigation is to insist on data portability clauses, to keep an integration abstraction layer between your code and the vendor SDK, and to track the cost of staying versus leaving on a yearly basis.

    What Increases Build Switching Cost

    Tribal knowledge that left with the original team, undocumented business rules encoded in stored procedures, custom protocols nobody wants to support, and tightly coupled internal systems that all assume the build will exist forever. The mitigation is documentation, modularization, and an explicit owner. Most internal builds fail this test by year three.

    The Hybrid Pattern That Usually Wins

    Most mature engineering orgs end up with a hybrid posture rather than a pure build or pure buy stance. The pattern looks like this: buy the commodity layers, build a thin orchestration layer on top, and reserve custom engineering for the differentiated workflow that touches the customer. Use Auth0 or WorkOS for identity, but build the tenant-specific authorization model that encodes your domain. Use Stripe for payments, but build the pricing engine that calculates what to charge. Use Snowflake for storage, but build the semantic layer your analysts and product team consume.

    This pattern works because it isolates vendor risk to interchangeable layers and concentrates engineering investment on the layer that compounds. Replacing Stripe with Adyen is painful but tractable. Replacing your pricing engine is a strategic project either way.

    The Anti-Pattern: Rebuilding the Commodity

    The most expensive mistake in this space is rebuilding undifferentiated commodity software because it feels strategic. We see it most often in three forms. The first is the in-house feature flag platform that started as a Friday afternoon hack and now consumes an SRE quarter every year. The second is the bespoke ETL pipeline built to avoid Fivetran or Airbyte license fees that ends up costing four times the annual contract in headcount. The third is the internal admin tool framework that reinvents Retool, Forest, or Appsmith because someone read a blog post about low-code being a trap.

    The pattern is recognizable. Engineers like the work, leadership likes the optionality, and finance does not see the line item because the cost is hidden inside payroll. Two years later, the system has one maintainer who cannot take vacation, no documentation, and a quiet plan to migrate to the SaaS that was rejected on day one

    Laptop on a wooden desk next to a notebook and a coffee cup
    Photo by Andrew Neel on Unsplash
    .

    The Decision Checklist

    When the tradeoff is genuinely close, work through these questions in order. The first one to flip the decision is the answer.

    1. Is this capability part of how we win in the market? If yes, build. If no, continue.
    2. Does a credible vendor exist with a clean API, documented data portability, and a track record beyond 5 years? If no, build or wait. If yes, continue.
    3. Will our usage in 24 months exceed the vendor’s pricing model breakpoint by a factor of 3 or more? If yes, model the breakeven. If no, buy.
    4. Do we have the operational maturity to own this system on-call for the next 5 years? If no, buy. If yes, continue.
    5. Does the build path require us to hire specialist talent we do not currently have? If yes, lean buy. If no, the decision is now financial, and TCO over 5 years decides.

    How to Run the Decision in Practice

    The framework is only useful if it produces a defensible decision in a finite amount of time. The version that survives contact with real organizations looks like a two-week exercise with three artifacts at the end: a one-page TCO model with documented assumptions, a one-page risk register that names the top three failure modes for each path, and a one-page recommendation that the sponsor signs. Anything more becomes a project. Anything less becomes a hallway conversation that gets re-litigated every quarter.

    The most common process failure is letting the analysis sprawl until the decision has been made by inertia. The team that spends six weeks evaluating six vendors at the line-item level is the team that ships a Frankenstein POC because nobody wanted to call the question. Set a deadline, name a decision-maker, and accept that the choice will be made with imperfect information. The cost of a wrong decision is recoverable. The cost of no decision is the year you spent not making it.

    When This Framework Applies

    This framework works for capabilities that are well-defined, have a vendor market, and are not currently a competitive crisis. It works for billing, identity, observability, support, internal tooling, data infrastructure, and most platform layers.

    When It Does Not Apply

    It does not apply to your core product surface. It does not apply to capabilities that no vendor sells because the market does not yet exist. It does not apply when regulatory constraints prohibit data leaving your environment, although in those cases the relevant choice is between self-hosted commercial software and pure custom build, not between commodity SaaS and custom. And it does not apply when speed to market is the only thing that matters and the build path adds 6 months you do not have. In that case, buy now, accept the lock-in, and revisit in 18 months with a real TCO model in hand.

    The discipline is to make the decision once, defend it with numbers, document the assumptions, and revisit those assumptions every two years. The teams that get this right are not smarter. They are more honest about which problems they are paid to solve.