Chapter 8: Reuse Patterns

saht reuse-patterns distributed-architecture sidecar shared-library service-mesh

Status: Notes complete


Overview

Code reuse is a fundamental engineering principle, but in distributed architectures it becomes significantly more complex. In a monolith, reuse is trivial — call a method, share a module. In a distributed system, “reuse” can mean anything from copying code to deploying a dedicated runtime service, and each approach carries dramatically different trade-offs in change management, performance, fault tolerance, and operational complexity.

Chapter 8 asks: what is the right mechanism for sharing code across services? The authors present four reuse patterns — Code Replication, Shared Library, Shared Service, and Sidecars/Service Mesh — and provide a framework for choosing among them. The central insight is that not all shared code is alike: infrastructure concerns (logging, monitoring, circuit breakers) have very different reuse characteristics than shared domain logic.

A key principle introduced: reuse via platforms — the idea that as a system matures, many shared concerns should be absorbed into the platform (infrastructure, service mesh) rather than owned by individual services or shared libraries.


Core Concepts

TermDefinition
Code replicationCopying shared code into each service that needs it; no runtime or build-time dependency
Shared libraryA versioned, compiled artifact (JAR, npm package, NuGet package) that services depend on at build time
Shared serviceA runtime service that encapsulates shared logic and is called via network by other services
SidecarA separate process/container deployed alongside each service, handling cross-cutting infrastructure concerns
Service meshA dedicated infrastructure layer (e.g., Istio, Linkerd) that manages service-to-service communication using sidecars
Change riskThe probability that a change to shared code will break consumers
Dependency managementThe practice of controlling which versions of a shared library each service uses
Semantic versioningVersion numbering (MAJOR.MINOR.PATCH) that communicates the nature of changes: breaking/non-breaking/bug-fix

Pattern 1: Code Replication

Description

The simplest approach: copy the shared code into every service that needs it. There is no shared artifact, no runtime dependency, no build-time coupling — each service owns its copy of the code.

When Replication Makes Sense

  • The shared code is small and unlikely to change (e.g., a simple utility function)
  • The code is used by only two or three services
  • Divergence is acceptable — each service may legitimately need to customize the logic for its context
  • The teams owning the services prefer zero coupling over consistency

Trade-offs

DimensionAssessment
SimplicityVery high — no infrastructure needed, no versioning, no dependency management
IndependenceMaximum — each service is completely decoupled from others
ConsistencyVery low — copies diverge over time; bug fixes must be manually applied to all copies
MaintenancePoor at scale — updating shared behavior requires touching every service
GovernancePoor — no central control; drift is invisible

Risks

  • Silent divergence: copies of code drift apart without anyone noticing, until behavior inconsistencies surface in production
  • Bug proliferation: a bug fixed in one copy must be manually propagated to all others — a process that is slow, error-prone, and easily forgotten
  • Does not scale: acceptable for 2-3 services sharing trivial code; unacceptable for 20 services sharing non-trivial business logic

When to Avoid

Avoid code replication when: the shared logic is non-trivial and likely to require bug fixes, when behavioral consistency across services is a business requirement, or when there are more than 2-3 consumers.


Pattern 2: Shared Library

Description

Extract shared code into a versioned, deployable artifact — a library — that services import as a build-time dependency. Examples: a Java JAR in a Maven/Gradle repository, an npm package, a NuGet package, a Python PyPI package, a Go module.

Versioning Strategies

The key challenge with shared libraries is managing change — how does a new version of a library reach all consumers without breaking them?

Three common versioning strategies:

  1. Versioned release (pinned version)

    • Each service pins to a specific version: my-library:2.3.1
    • Services upgrade on their own schedule — no forced upgrade
    • Risk: consumers lag far behind the current version; support burden grows
    • Good for: stable libraries with infrequent releases
  2. Semantic versioning with deprecation policy

    • Follow SemVer (MAJOR.MINOR.PATCH): breaking changes = major bump
    • Maintain a deprecation policy (e.g., “old major versions supported for 6 months after new major release”)
    • Services must upgrade before deprecation deadline
    • Good for: widely-used libraries with active development
  3. Single version (no versioning)

    • All consumers must use the current version — no pinning
    • Library changes are backward-compatible always, or all consumers are updated in sync
    • Good for: single-team ownership of all consumers (rare in large distributed systems)

Change Control Challenges

The hardest problem with shared libraries: who owns the library, and what is the process for changing it?

  • If one team owns the library and many teams depend on it, the owning team becomes a bottleneck
  • If any team can change the library, breaking changes can impact many consumers unexpectedly
  • The solution is a change control process: breaking changes require advance notice, versioning, migration guides, and consumer sign-off before release

Trade-offs

DimensionAssessment
SimplicityModerate — requires artifact repository, versioning discipline, dependency management
IndependenceGood — services can upgrade on own schedule (with pinning)
ConsistencyGood when pinned versions are current; poor when consumers lag significantly
PerformanceExcellent — in-process call, no network overhead
Change managementThe core challenge — requires versioning discipline and governance
Deployment couplingLow for consumers; the library itself requires no deployment (build-time only)
Fault toleranceExcellent — no network call; in-process

When to Use Shared Library

  • Shared code is infrastructure/utility in nature (logging, retry logic, serialization helpers, common DTOs)
  • Shared code is domain logic that is stable and changes infrequently
  • The shared code must be performant (cannot afford a network call)
  • Services are written in the same language/platform (cross-language libraries are extremely difficult)
  • You have the governance discipline to maintain versioning and change control

Limitations

  • Does not work well across multiple languages/runtimes — each language needs its own library implementation
  • Cannot enforce runtime upgrades — consumers may stay on old versions indefinitely
  • Dependency version conflicts: services with complex dependency trees can experience version conflicts when multiple libraries transitively depend on different versions of the shared library

Pattern 3: Shared Service

Description

Extract shared logic into a standalone runtime service that other services call via the network (HTTP, gRPC, message queue). The shared service is independently deployed, scaled, and owned.

When Shared Service Makes Sense

  • The shared code contains runtime-variable behavior (e.g., logic that changes based on live configuration, feature flags, or database state)
  • The shared code needs to be polyglot — usable across services written in different languages
  • The shared logic has significant computational requirements that justify independent scaling
  • The shared logic requires independent security controls (e.g., it accesses sensitive data)
  • Centralized control of a shared business function is a strong requirement

Trade-offs

DimensionAssessment
Language agnosticExcellent — any service that can make an HTTP/gRPC call can consume it
Change managementGood — API versioning; consumers are insulated from implementation changes
Runtime updatabilityExcellent — change the service, all consumers get the new behavior without redeploying
PerformancePoor — every call crosses a network boundary; adds latency (10-100ms per call)
ScalabilityThe shared service becomes a potential bottleneck if not scaled correctly
Fault tolerancePoor — the shared service is a single point of failure; if it goes down, all consumers are impacted
Operational complexityHigh — requires its own deployment pipeline, monitoring, SLA, scaling policy

The Fault Tolerance Problem

The shared service pattern introduces a critical fault tolerance concern: it becomes a shared dependency point. If the shared service fails or becomes slow:

  • All consumers are affected simultaneously
  • A bug in the shared service can cascade to many services at once
  • Network partitions between the consumer and shared service block functionality

Mitigation: circuit breakers on all consumers, redundant deployment (multiple instances, multiple availability zones), comprehensive health checks, and a clear SLA ownership model.

The Performance Problem

Every call to the shared service crosses a network boundary. In scenarios where the shared logic is called frequently (e.g., an authorization check on every API call), the cumulative latency impact can be significant. Caching results on the consumer side can mitigate this but introduces cache invalidation complexity.

Change Risk

Compared to a shared library, a shared service has higher change risk in one sense: a buggy deployment of the shared service immediately affects all consumers at runtime — there is no version pinning to protect consumers. This requires robust testing, canary deployments, and backward-compatible API changes.

When to Avoid Shared Service

Avoid when: the shared logic is called on the critical performance path (high-frequency, low-latency requirement), when the shared logic is simple enough that a library suffices, or when the operational overhead of an additional service is not justified by the sharing requirement.


Pattern 4: Sidecars and Service Mesh

Description

The sidecar pattern deploys a separate, lightweight process alongside each service (in the same pod in Kubernetes, or the same VM) to handle cross-cutting infrastructure concerns. The sidecar handles concerns like: logging, metrics collection, distributed tracing, service discovery, health checking, circuit breaking, mTLS encryption, and rate limiting.

A service mesh (e.g., Istio, Linkerd, Consul Connect) is a platform that coordinates all sidecars across the system — providing a control plane for configuring network policies, observability, and security uniformly across all services.

What Problem Does This Solve?

Without sidecars, every service must implement infrastructure concerns directly:

  • Each service imports a logging library AND a metrics library AND a tracing library AND a circuit breaker library
  • When you want to add a new infrastructure capability (e.g., distributed tracing), every service must be updated and redeployed
  • If services are polyglot, you need equivalent libraries in every language
  • Infrastructure libraries pollute service codebases with non-domain code

The sidecar solution:

  • Infrastructure concerns are delegated to the sidecar process
  • The service code handles only domain logic — no infrastructure imports needed
  • Infrastructure upgrades are done by updating the sidecar, not the service
  • Works uniformly across polyglot services — the sidecar is language-agnostic

Sidecar vs. Shared Library vs. Shared Service

ConcernShared LibraryShared ServiceSidecar
Type of logicBusiness or infraBusinessInfrastructure only
CouplingBuild-timeRuntime (network)Runtime (local IPC)
Language supportSame language onlyAny languageAny language
PerformanceIn-process (best)Network call (worst)Local IPC (very fast)
Update mechanismLibrary upgrade + redeployDeploy new service versionUpdate sidecar (no service redeploy)
Fault couplingNoneHigh (shared failure point)None (sidecar is local)
Operational overheadLowHighMedium (platform manages it)

What the Service Mesh Adds

The service mesh control plane provides:

  • Centralized policy enforcement: apply a new circuit-breaking policy to all services without touching service code
  • Uniform observability: all traffic flows through sidecars → complete topology visibility, latency metrics, error rates
  • mTLS everywhere: mutual TLS between all services enforced at the sidecar layer — no service-level certificate management
  • Traffic management: canary deployments, A/B testing, traffic shifting controlled at the mesh level
  • Security policies: fine-grained authorization rules (service A can call service B on endpoint X only)

Trade-offs

DimensionAssessment
Infrastructure couplingNear zero — services are unaware of infrastructure concerns
Language agnosticExcellent — sidecar speaks to service via localhost
PerformanceVery good — local IPC (loopback), not a network call
Operational overheadHigh upfront — service mesh is complex infrastructure to operate
Update agilityExcellent — update infrastructure without touching service code
Fault isolationExcellent — sidecar failure affects only one service (not a shared dependency)
Learning curveHigh — Istio/Linkerd are complex platforms
StandardizationVery good — enforces uniform behavior across all services

When to Use Sidecars

  • The system has many services (10+) — amortizing the mesh overhead is only worthwhile at scale
  • Services are polyglot — a single infrastructure library cannot serve all languages
  • Infrastructure concerns are evolving rapidly — you want to update logging/tracing/security without touching service code
  • The organization wants to enforce uniform standards (security, observability) across all services without relying on every team to implement them correctly
  • Compliance and security requirements demand uniform mTLS and audit logging

When to Avoid Sidecars

  • Small number of services — the mesh overhead is not justified
  • Homogeneous language environment — a shared library may be simpler and sufficient
  • Team lacks platform engineering expertise to operate a service mesh

The Four Patterns Compared

Summary Trade-off Table

PatternBest ForPerformanceChange MgmtLang SupportFault RiskComplexity
Code ReplicationTiny, stable, divergence-OK codeExcellentVery poorAllNoneVery low
Shared LibraryStable logic, same-language, performance-criticalExcellentModerateSame languageNoneModerate
Shared ServiceRuntime-variable logic, polyglot, independent scalePoor (network)GoodAllHigh (shared SPOF)High
Sidecar/MeshInfrastructure concerns, polyglot, large systemsVery good (IPC)ExcellentAllLow (local)Very high

Decision Flowchart

What kind of code needs to be shared?
│
├── Infrastructure concern (logging, tracing, circuit breakers, mTLS)?
│   └── → Use SIDECAR / SERVICE MESH (if at scale)
│       └── If small scale → Use SHARED LIBRARY (infrastructure library)
│
└── Domain / Business logic?
    │
    ├── Small, stable, few consumers, divergence acceptable?
    │   └── → Consider CODE REPLICATION
    │
    ├── Stable, same language, performance-critical, good governance?
    │   └── → Use SHARED LIBRARY
    │
    ├── Runtime-variable, polyglot, needs independent scaling/security?
    │   └── → Use SHARED SERVICE
    │       └── But: add circuit breakers, plan for SPOF, accept latency cost
    │
    └── Extremely performance-critical AND polyglot?
        └── → No perfect answer; consider language-specific libraries per runtime

When Does Reuse Add Value? — Reuse via Platforms

The authors make an important distinction: not all reuse is equal. Reuse adds value when it:

  1. Prevents inconsistent behavior that would cause bugs or compliance failures
  2. Reduces operational burden (teams don’t each implement logging/monitoring from scratch)
  3. Enforces standards (security, observability) uniformly

Reuse does not add value — and may harm the system — when it:

  1. Couples services unnecessarily (shared library with frequent breaking changes)
  2. Creates shared points of failure (shared service called by everything)
  3. Forces homogeneity where diversity is appropriate (services legitimately need different behavior)

The mature answer for infrastructure reuse is platforms: invest in a service mesh, a central logging platform, a tracing platform, and a secrets management platform. Teams consume these platforms rather than implementing concerns individually. This is reuse through infrastructure, not code.


Sysops Squad Saga

The Sysops Squad story in Chapter 8 covers two types of shared code: common infrastructure logic and shared domain functionality.

Shared Infrastructure Logic

Context: The Sysops Squad team realizes that all services need: logging, distributed tracing, circuit breaker logic, service discovery, and mTLS. Initially implemented as a shared library, this creates problems: not all services are in the same language, the infrastructure team wants to update tracing without triggering service redeployments, and services teams are frustrated maintaining infrastructure code.

Solution chosen: Move to a sidecar pattern with a service mesh. Infrastructure concerns are extracted entirely from service code. Each service gets a sidecar proxy (the book uses the term “service plane”) that handles all infrastructure cross-cutting concerns.

Result: Service teams write only domain logic. The platform team controls infrastructure upgrades. Updating distributed tracing across all 15 services requires deploying a new sidecar version — zero service code changes.

Lesson: At sufficient scale, the sidecar/service mesh pattern eliminates a class of coupling (infrastructure-to-service) that shared libraries can never fully eliminate.


Shared Domain Functionality

Context: The customer notification logic — the rules for when to notify customers, via which channel, and with what content — is needed by multiple services: the ticket service (ticket status updates), the survey service (survey invitations), and the billing service (invoice notifications).

Options considered:

  • Shared library: the notification rules change when business decisions change; deploying a library update and redeploying all consumers is cumbersome
  • Code replication: risk of rules diverging across services — a customer gets inconsistent notifications
  • Shared service: centralizes the notification logic; all consumers call the notification service

Solution chosen: Shared service for the domain notification logic. One notification service owns the rules for when and how to notify. Consumer services call it when they want to trigger a notification. This ensures consistency (same rules for all notification types), allows the notification rules to be updated by deploying only the notification service, and is polyglot-friendly.

Mitigation of shared service risks:

  • Circuit breakers on all consumers to prevent cascading failure
  • Asynchronous notification (put events on a queue; notification service processes them) to decouple notification from the critical path

Lesson: Shared domain logic that requires consistent behavior across services and changes driven by business decisions (not engineering decisions) often warrants a shared service — accepting the performance and fault tolerance trade-offs.


Key Takeaways

  1. Code replication is a last resort for trivial code only: acceptable for tiny, stable utilities used by 2-3 services where divergence is fine. Fails at scale when the code changes.

  2. Shared libraries work well for stable, same-language code: the dominant challenge is change management — versioning, deprecation policies, and change control processes are essential for library governance.

  3. Shared services enable runtime polymorphism and polyglot support: at the cost of performance (network call), fault tolerance (shared SPOF), and operational complexity. Always add circuit breakers.

  4. Sidecars decouple infrastructure from services: by handling cross-cutting concerns in a separate process, services stay clean of infrastructure code, and infrastructure can be updated independently.

  5. Service mesh = coordinated sidecars: the mesh control plane enables uniform policy enforcement, observability, and security across all services — this is the mature infrastructure-as-code approach to operational concerns.

  6. Infrastructure vs. domain logic requires different reuse patterns: infrastructure concerns (logging, tracing, circuit breakers) → sidecar/mesh. Domain logic (business rules, shared calculations) → shared library or shared service, depending on change rate and polyglot requirements.

  7. Change management is the hardest problem in shared libraries: the discipline required (semantic versioning, deprecation policies, change control) is often underestimated. Without it, shared libraries create worse coupling than the code they were meant to consolidate.

  8. The shared service is a shared point of failure: every consumer’s availability now depends on the shared service’s availability. Design accordingly — multiple instances, cross-region deployment, circuit breakers, async consumption where possible.

  9. Reuse via platforms is the architectural ideal: rather than each team implementing infrastructure, invest in shared platforms (logging, tracing, service mesh, secrets management) that provide reuse through infrastructure rather than code. This is what mature platform engineering teams deliver.

  10. Choosing the wrong reuse pattern creates architectural debt: using code replication for non-trivial logic creates consistency debt; using shared service for performance-critical logic creates latency debt; using shared library across polyglot services creates maintenance debt.


Last Updated: 2026-05-30