Technical Excellence

Who Pays When It Varies

Part IV · Design Cross-cutting concerns

In a charter I wrote for a backend team in 2020, one line on my own list of responsibilities holds a whole position on how engineering should be organised: "Tech Stack, Development Process and SDLC Independence but Cloud Operational Consistency and Dependency."

On one side of that "but", a team chooses its own tools and its own way of working. On the other, the way its software runs in the cloud is the same for every team. The same notes turn the sentence into two lists, and at no point do they say why an item sits on the side it does.

// the crux

Let the team choose what the team pays for. Decide once what someone else pays for.

// in one breath
  • Two lists from 2020 that divide a team's decisions from everyone's, and the one item my own notes put on both sides.
  • The question those lists never asked, and why it sorts every row.
  • How a starter project made the consistent choice the easy one without turning into a shared library.
two lists

Two Lists, and One Item on Both

A microservices brief in the same notes names the split outright, as the distinction between independent and dependent decision making, and lists both sides.

// the team decides
  • Tech stack
  • Development and build strategy
  • Testing strategy
  • Static code analysis
  • Code merging and pull requests
  • CI/CD pipeline
  • Cloud services
// decided once
  • Deployment and monitoring strategy
  • Data pipeline
  • Cross-cutting concerns
  • Enterprise security
  • Security profiling, load and performance testing
  • Performance SLI, SLA and SLO
From the microservices brief in my notes, in its own order. The brief calls the two sides independent and dependent; the labels here are mine.

Read the documents against each other and one item turns up on both sides. The brief puts deployment on the dependent list, next to monitoring. A post I drafted that year says a team must be independent in its "Deployment Architecture and Strategy". The charter's own line separates the development process, which is independent, from cloud operations, which are not, and deployment is the step that joins the two.

Three documents from the same hand, and deployment sits in three places. Nothing in any of them says where the line comes from, and without that, a row nobody has listed yet has no side to go to.

the missing reason

Who Pays When It Varies

The reason I would write beside those lists now is one question. When two teams do this differently, who pays for the difference?

Start with the first list. If one team writes Kotlin and the team next to it writes Go, the difference is paid for by the team that chose: in its hiring, its builds and its own learning. The same goes for a testing strategy, a merge rule or a pipeline, and for which managed database a service uses, because the team that picked it is the team that lives with it. Variety on that list costs only the people who wanted it, which makes them the right people to decide.

The second list behaves differently. When one team invents its own log format, the bill goes elsewhere: to the engineer from another team following one failed request across several services, to the pipeline that has to parse every format anyone ever chose, and to the dashboard that cannot add two services together because they named the same metric two ways. When a team shapes its errors its own way, every client that calls it pays. Every Error Has an Address makes the case for one error catalogue; this is the reason it has to be one.

That is the rule the lists were missing. A decision whose cost stays inside a team belongs to that team. A decision whose cost crosses the boundary gets made once, for everyone, because the people who would pay for it were never in the room where a team made it.

The same question settles deployment. How a team builds and ships its artifact is paid for by that team, in its own lead time. Where the artifact runs, how it reports its health, and what the engineer on call sees when it fails are paid for by whoever carries the pager that night. Deployment turned up on both lists because the line runs through the middle of it. It also explains the charter's pairing of cloud services with cloud operations: which service a team uses is its choice, and how that service is run is everyone's.

// who pays when it varies
Decision
Who pays when it varies
Decided by
Language and framework
The team that chose it
The team
Testing strategy and merge rules
The team
The team
The build pipeline
The team, in its own lead time
The team
Which managed service to use
The team that lives with it
The team
Log format
Whoever follows a request across services
Once
The shape of an error
Every client that calls the service
Once
Metric names
Whoever adds services together on a dashboard
Once
The trace header
Every request that crosses a service
Once
How a service runs and reports its health
Whoever is on call
Once
The rows come from my notes. The middle column is the question they never asked.
the starter

The Starter Every Service Copied

A list on its own makes nothing consistent. The same charter also gave me the job of "Providing A Base Project As Baseline for Backend Development of APIs". I built it, and new services started from it.

It carried the parts every service would otherwise have written its own way: a validation wrapper, a proxy wrapper for how services call each other, a cache wrapper and a Kafka wrapper, with the common modules and a parent POM around them. A team that began from the base project had the consistent version of the expensive things on its first day, and kept every decision on the first list for itself.

The industry has names for this kind of starter. At Netflix, the common platform and tooling that central teams build for every development team is the paved road, and each team keeps the freedom to leave it. Spotify described its golden paths in August 2020 as "the opinionated and supported path" to building something, written after a culture of autonomous teams had left it with what it called rumour-driven development. My base project was the same idea on a much smaller scale: consistency that arrives as the easiest thing to do, instead of as a meeting a team has to attend.

the copy

What Must Not Drift

There is an obvious objection, and I have made it myself. In When Architecture Becomes a Leadership Problem I told teams to resist a shared library for everything in common, because every shared dependency is a coupling point. A base project full of wrappers could look like exactly that.

It was not one, because nothing depended on it. A new service copied the base project when it was created and owned its copy from then on, so no release of mine could ever break a team's build.

A copy has a price of its own. Every team that owns its copy is free to change it, and over time some will. So what has to stay the same across services is the contract the code produces rather than the code itself: the fields on a log line, the shape of an error body, the names of the metrics, the header that carries a trace from one service to the next. A contract can be checked wherever services meet, at the edge, in the pipeline that reads the logs, or in a test against the agreed shape, whatever language produced it.

The trace header is the cleanest case. The W3C made Trace Context a Recommendation in February 2020, a standard for how that context is sent and modified between services. It is a format, and a service written in any language can honour it without importing a line of anyone else's code.

// the part worth keeping

Inside one deployable, share the code. Across deployables, share the contract.

six years later

Six Years Later, One Level Down

The standards I work to now carry the same split, one level down. Their default for a back end is a single deployable with one module per bounded context, and their rule for everything the modules share reads: "Cross-cutting concerns (errors, logging, correlation, metrics, security, transactions) are implemented once, in a shared kernel, and used by every module. Each module decides its own internals."

Inside one deployable that kernel can be code, because everything that uses it ships together and no copy of it can wander off on its own. Between deployables the same concerns travel as a contract. The list itself barely moved in six years. Errors, logging, metrics and transactions are on both. Correlation joined it, and so did security, which the brief had kept on a line of its own. What changed is the unit on each side of the line: a team and its services then, a module and its internals now.

The question underneath did not change at all.

I drew that line in 2020 and left the reason out. The reason is the part that travels, from teams to modules and to whatever the unit turns out to be next. Ask who pays when a decision varies, and most rows sort themselves before anyone has to argue for them.
// carry forward

A boundary you standardise is a boundary you have already drawn. Part IV continues with where those boundaries come from: the words the business actually uses, and why bounded contexts end up as team boundaries.

// continue exploring