Build vs. Integrate: The Trades (and Biases) We Ignore

Build vs. Integrate is a question we often ask ourselves (or we should). Integration trades less invention for more coordination. Measure two clocks (build time and integration time) plus their variance, then decide.

Software teams love to ship. Some of us love to build the perfect thing; others love to integrate and compose systems out of proven parts. Both instincts are useful, and both can burn months when we don’t think clearly about the trades.

The core idea

Every decision sits on two ledgers:

  • Technical ledger: time-to-first-value, performance, API/ABI stability, security, licensing, packaging, testability, operability.
  • Psychology ledger: why we want to roll our own (or import something) and how bias quietly distorts the numbers.

If you don’t account for both, your “build vs. integrate” call is a coin toss dressed up as engineering.

Build vs. Integrate: Why building from scratch feels so good (and sometimes isn’t)

  • The IKEA effect. We overvalue what we build ourselves—even when it’s objectively similar to an off-the-shelf option. That’s been demonstrated repeatedly in lab studies.
  • Endowment + status-quo bias. Once we’ve started down a path (or already own code), we irrationally prefer sticking with it, and we overprice what we already have.
  • “Uniqueness” bias. We tell ourselves our project is special and standard parts won’t fit. The fix is to force an outside view—assume someone solved your problem before and go find it.
  • Not-Invented-Here (NIH). It’s real, and it hides in rational language about control and quality. Open innovation literature shows the upside of using outside ideas when you can.

These are features of human cognition, not defects in your character. The antidote is a fast, disciplined evaluation process.

Why integrating feels cheap (and sometimes isn’t)

Integration has its own hard costs:

  • Supply-chain risk. Log4Shell and the 2024 xz backdoor weren’t theoretical, they were real, expensive incidents. If “just pull a library” is your security plan, it’s not a plan.
  • Licensing. Permissive vs. copyleft obligations matter (static vs. dynamic linking, derivative works, attribution, etc.). Read the license; when in doubt, ask counsel. The FSF’s GPL FAQ and neutral overviews are good starting points. (This is not legal advice.)
  • Versioning and ABI stability. SemVer communicates API intent, not magic compatibility.
  • Build/packaging friction. The first day with a new dependency is rarely “copy/paste from README.” You’ll pay for toolchain setup, CI cache hygiene, and transitive dependency churn.

C++-specific integration traps (that bite even seniors)

  • Binary boundaries. Passing ownership across DLL/shared-object lines is risky. Allocate and free in the same module, avoid throwing exceptions across boundaries, and be careful with STL types like std::string across different runtimes.
  • libstdc++ dual ABI / std::string. GCC’s dual ABI and cross-lib issues can surprise you when mixing toolchains or distros. Pin, document, and test your ABI surface.

Practical mitigations

  • Prefer C-style, versioned ABIs at external boundaries: POD structs, const char* + length, explicit create/destroy functions.
  • Contain third-party code behind adapters; don’t let it leak through your public headers.

A Tiny C++ Pattern: Contain the Dependency

Use an adapter to hide a JSON library (swap simdjson for nlohmann::json later if needed).

// domain.h
struct Telemetry {
    std::string id;
    double temperature;
};

struct IJsonTelemetry {
    virtual ~IJsonTelemetry() = default;
    virtual std::vector<Telemetry> parse(std::string_view payload) = 0;
};
// simdjson_adapter.cpp
#include "domain.h"
#include <simdjson.h>

class SimdjsonTelemetry final : public IJsonTelemetry {
    simdjson::ondemand::parser p_;
public:
    std::vector<Telemetry> parse(std::string_view s) override {
        std::vector<Telemetry> out;
        auto doc = p_.iterate(s);
        for (auto t : doc.get_array()) {
            Telemetry v;
            v.id = std::string(t["id"].get_string().value());
            v.temperature = double(t["temp_c"].get_double().value());
            out.push_back(std::move(v));
        }
        return out;
    }
};

// factory (keeps the dependency out of public headers)
std::unique_ptr<IJsonTelemetry> make_json_telemetry() {
    return std::make_unique<SimdjsonTelemetry>();
}

simdjson is fast and mature if you need it; nlohmann::json is ergonomic. Both are solid—pick based on your non-functional needs, but keep the seam. (GitHub)

If you must cross a binary boundary, expose a C API:

extern "C" {
    typedef struct { const char* id; double temperature; } TelemetryC;

    void* json_telemetry_create();
    void  json_telemetry_destroy(void* h);
    // returns number of items and fills *out with malloc'ed array; caller frees with json_telemetry_free
    size_t json_telemetry_parse(void* h, const char* data, size_t len, TelemetryC** out);
    void   json_telemetry_free(TelemetryC* p);
}

This avoids exceptions and STL types at the ABI seam.


Integration Is Not Free: The Two Clocks

There are two distinct clocks on every “build vs. integrate” decision:

T_build =
  t_spec + t_impl + t_tests + t_perf + t_docs + t_ops

T_integrate =
  t_discovery + t_eval + t_adapter + t_buildsys + t_tests +
  t_perf + t_docs + t_ops + t_security + t_compliance
  • t_adapter: type/ABI/ownership bridges, error-model translation, threading, configuration, logging/metrics, and observability glue.
  • t_buildsys: package manager recipes, transitive deps, toolchain quirks, CI cache hygiene.
  • t_security / t_compliance: SBOM, license review, CVE tracking, update treadmill.

Rule of thumb

  • If the dependency is commodity and t_adapter is small (clean C/C++ surface, stable releases), T_integrate << T_build.
  • If the dependency leaks heavy frameworks/types across the boundary (Qt types in a non-Qt core, templated singletons, exceptions across DLLs), t_adapter dominates and cancels the calendar win.

Make the uncertainty visible: the variance of T_integrate is usually higher than T_build. Spike early and measure t_adapter and t_buildsys in hours, not vibes.

Measure in hours, not vibes.

The Adapter Tax (C++ Edition)

Common time sinks you will pay when integrating:

  • Encoding & text: UTF-16/UTF-8 boundaries (QStringstd::string, Windows APIs, POSIX).
  • Ownership & lifetime: who allocates/frees? Are allocators compatible? Are exceptions allowed across the seam?
  • Binary seams: STL types across different CRTs/standard libs; RTTI/exceptions through DSOs/DLLs.
  • Error model: exceptions vs. expected<T, E> vs. error codes.
  • Threading model: library requires its own threads/event loop (Qt), or expects to be called from a specific context.
  • Time/clock source: qint64 msec vs. std::chrono types.
  • Build & packaging: Conan/vcpkg profiles, static vs. shared, LTO, link order, toolchain pinning.
  • Observability: integrate with your logging/metrics/tracing without pulling theirs through your domain.

C++: Minimal, Safe Bridges (Qt & STL)

Keep conversions explicit and isolated. Don’t let framework types leak into your domain.

// utf8_bridge.h
#pragma once
#include <QString>
#include <string>
#include <string_view>

inline std::string to_utf8(const QString& s) {
    const QByteArray ba = s.toUtf8();
    return std::string(ba.constData(), static_cast<size_t>(ba.size()));
}

inline QString from_utf8(std::string_view s) {
    return QString::fromUtf8(s.data(), static_cast<int>(s.size()));
}

// Prefer u16 when you control both sides:
inline std::u16string to_u16(const QString& s) { return s.toStdU16String(); }
inline QString from_u16(const std::u16string& s) { return QString::fromStdU16String(s); }

Hide third-party types behind ports/adapters:

// domain.h (your surface)
struct Telemetry { std::string id; double temp_c; };

struct ITelemetryParser {
    virtual ~ITelemetryParser() = default;
    virtual std::vector<Telemetry> parse(std::string_view payload) = 0;
};
// qt_json_parser.cpp (integration module)
#include "domain.h"
#include "utf8_bridge.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>

class QtTelemetryParser final : public ITelemetryParser {
public:
    std::vector<Telemetry> parse(std::string_view payload) override {
        const auto doc = QJsonDocument::fromJson(QByteArray::fromRawData(payload.data(),
                                                                         int(payload.size())));
        std::vector<Telemetry> out;
        for (const auto& v : doc.array()) {
            const auto o = v.toObject();
            out.push_back(Telemetry{
                to_utf8(o["id"].toString()),
                o["temp_c"].toDouble()
            });
        }
        return out;
    }
};

std::unique_ptr<ITelemetryParser> make_qt_parser() {
    return std::make_unique<QtTelemetryParser>();
}

Binary boundary? Use a C ABI:

extern "C" {
    struct TelemetryC { const char* id; double temp_c; };

    void*  parser_create();
    size_t parser_parse(void* h, const char* data, size_t len, TelemetryC** out); // caller frees
    void   parser_free(TelemetryC* p, size_t n);
    void   parser_destroy(void* h);
}

No STL/exceptions across module seams; document allocator rules.

Testing Cost Is Different, Too

Integration adds contract tests and compatibility tests:

  • Contract tests at the adapter: golden inputs/outputs, property tests, fuzz harness (e.g., libFuzzer on the C seam).
  • Compatibility matrix: compilers (MSVC/Clang/GCC), standard libs, OSes, and the dependency’s supported versions.
  • Perf guardrails: microbench the seam (serialization, conversions, thread hops) so you can budget the overhead.

Template for a quick seam test (gtest + fuzz entry optional):

TEST(QtTelemetryParser, ParsesNominal) {
    auto p = make_qt_parser();
    const std::string json = R"([{"id":"A","temp_c":21.5}])";
    const auto v = p->parse(json);
    ASSERT_EQ(v.size(), 1u);
    EXPECT_EQ(v[0].id, "A");
    EXPECT_DOUBLE_EQ(v[0].temp_c, 21.5);
}

Security & Dependencies: The Update Treadmill

Integration obligates you to:

  • Generate an SBOM in CI, track transitive deps, and pin versions.
  • Budget time every sprint for updates (tiny, frequent merges beat annual crises).
  • Add supply-chain checks (sig/attestations where available) and watch CVE feeds.

These are recurring costs in t_security and t_ops, not one-time fees.

Tools: SBOM (Syft/CycloneDX), vuln scan (OSV-Scanner/Trivy), sigs/attestations (Sigstore), dependency alerts (OSV/GitHub advisories).

Build vs. Integrate Quick Estimator: Will Integration Actually Save Time?

Score each 0-3 (lower is better). If IntegrateBuild by ≥ 4 points, integrate. If close, spike both and re-score.

Integration

  • Surface is C/POD, stable versions, good docs/tests
  • Adapter touches ≤ 3 concerns (types, errors, threads)
  • Package/CI is standard (vcpkg/Conan recipe exists)
  • Project health is strong (maintainers, cadence)
  • License is permissive/compatible

Build

  • Domain is differentiator (core IP/perf/latency)
  • Small scope (≤ 2 person-weeks to MVP)
  • You control ABI/time/allocators fully
  • Fewer external moving parts to certify

Patterns That Keep Either Choice Cheap

  • Ports & Adapters: domain first, edges replaceable.
  • Strangler: façade + incremental replacement to keep risk/time bounded.
  • Narrow public surface: one small header, one factory, one result type.
  • No framework types in your public headers.

Build vs. Integrate: The Takeaway

Integration trades less invention for more coordination. Sometimes that’s a slam dunk; sometimes the adapter, build, and security taxes erase the win. Put numbers on both clocks, prototype the seam, and keep your domain isolated so you can change your mind without a rewrite.


Making the Build vs. Integrate Decision

Build vs. integrate: a 60-minute pre-mortem

Score each dimension 0–5 (lower is better). Weight by your context.

DimensionIntegrate existingBuild from scratch
Time-to-first-value
Capability fit (80/20 ok?)
Ongoing maintenance (3y)
Security posture / SBOM
License risk
Performance headroom
API/ABI stability needs
Team familiarity
Vendor/project health
Testability & CI friction

When the totals are close, prefer integration for commodity problems and build when it’s a core differentiator (or when licenses/security forbid integration). Brooks reminded us there is no silver bullet, don’t chase one by defaulting to greenfield.

A simple TCO estimator (3-year)

TCO = DevHours_initial
    + DevHours_annual_maint * 3
    + (SecurityUpdates/yr * Cost_per_update * 3)
    + IntegrationFriction (build/CI/release)
    + License/Compliance overhead
  • For integration, DevHours_initial usually drops, but IntegrationFriction and SecurityUpdates/yr can spike.
  • For custom builds, DevHours_initial is high; if it’s your differentiator, you may want that spend.

Architecture patterns that lower regret

  • Hexagonal (Ports & Adapters). Keep your domain pure; push details (DBs, UIs, libraries) to the edges behind ports. It makes both integration and rewrites cheaper.
  • Strangler Fig. If you must replace something, do it incrementally behind a façade. Ship slices, not a big-bang rewrite.

Choosing to Build vs. Integrate

When I choose to BuildWhen I choose to Integrate
– It’s a differentiator (algorithm, protocol, UX) and I need control

– The license is incompatible with my distribution model

– I need deterministic latency / hard realtime behavior the library can’t guarantee

– The library’s governance and release cadence look shaky

Small scope (≤ 2 person-weeks to MVP)

– You control ABI/time/allocators fully

– Fewer external moving parts to certify
– It’s a commodity (JSON/TOML parsing, HTTP clients, TLS, ZIP, image codecs)

– There’s a healthy project & ecosystem with explicit versioning and security process

– I can isolate it behind a narrow adapter and keep my domain clean

– The API surface is POD, stable versions, good docs/tests

– Adapter touches ≤ 3 concerns (types, errors, threads)

Package/CI is standard (vcpkg/Conan recipe exists)

– Project health is strong (maintainers, cadence)
License is permissive/compatible

Build vs. Integrate: Closing Thoughts

We often frame this as a technical question. It is—but your psychology votes, too. Put numbers to both ledgers, contain risk at the boundaries, and deliberately choose where your team spends its creativity.

That’s how you ship faster and sleep better.


Discover more from John Farrier

Subscribe to get the latest posts sent to your email.

One thought on “Build vs. Integrate: The Trades (and Biases) We Ignore

  1. Great article! I really like the idea of creating a seam and keeping application-specific code isolated so that you can change libraries without a big rewrite. This helps maintain a clean, modular architecture by de-coupling application code from third-party implementation details. I ran into a similar issue in the past when switching between libraries for point cloud processing (e.g., PCL vs CGAL).

Leave a Reply

Discover more from John Farrier

Subscribe now to keep reading and get access to the full archive.

Continue reading