These are ordered by how expensive they are to reverse, not by how often they occur. The cheap ones you can fix in a sprint once you notice. The expensive ones require unwinding decisions that other teams have built months of work on top of, which means they require political capital you may not have, which is why they persist for years in organisations that know perfectly well they are there.

Every one of these looks defensible in a design document. That is the point — none of them are stupid ideas. They are reasonable ideas whose failure mode only appears at a scale or a time horizon the original decision did not consider.

1. The mandatory abstraction over a mature interface

The platform wraps the Kubernetes API behind a simplified custom resource, and direct access is removed. The intent is good: application teams should not need to know what a PodDisruptionBudget is.

What happens is that the wrapper exposes a subset of the underlying API, and the subset is defined by what the platform team imagined teams would need in the quarter they built it. Then a team needs a topology spread constraint. Then another needs an init container. Then someone needs a specific annotation for the ingress controller. Each request is a platform-team ticket, a schema change, a release, and a rollout — for a field that already existed in the API you hid.

Two years later the custom resource has 140 fields, all of which are pass-throughs to the Kubernetes API with different names, and you are maintaining a lossy translation layer over a stable, well-documented, widely-understood interface.

Why it is expensive to reverse: every service in the company is defined in your schema. Migrating means rewriting all of them and retraining everyone.

Do instead: generate manifests rather than hiding them. Give teams a tool that produces a complete, readable, checked-in Kubernetes manifest from a short input, and let them edit the output when they need to. The abstraction is in the generator, not in a permanent runtime layer. Teams who never need to look at the output never do; teams who do are not blocked. Kubernetes is a mature interface — hiding it is hiding a skill your engineers benefit from having.

2. Configuration that generates configuration that generates configuration

A values file feeds a Helm chart that renders a Kustomize base that is patched per environment by an overlay, which is templated by a wrapper script. Four layers, and finding out why a pod has the wrong memory limit requires mentally executing all of them.

The tell is a specific question: how long does it take a mid-level engineer to answer "what is the actual final value of this field in production, and which file set it?" If the answer is more than two minutes, you have this.

Do instead: one layer of templating, maximum. Make the rendered output an artifact — check it in, diff it in the PR, and show it in the review. GitOps tools that display the rendered diff before sync are worth a great deal here. If you need programmatic generation, use a real language with types (CUE, or a small Go or TypeScript program) rather than stacking text templating engines, because the failure mode of templated YAML is a valid document with the wrong semantics.

3. Self-service that is a ticket queue in a costume

There is a portal. The portal has a form. Filling in the form creates a Jira ticket assigned to the platform team. The average time to fulfilment is three days.

This is worth naming precisely because everyone involved believes they built self-service. The portal is real, the automation behind some of it is real, and the metric being reported is "requests through the portal," which is going up.

Measure the right thing: p50 and p95 time from request to usable resource, and the fraction of requests that complete with zero human involvement. If that fraction is below about 90 percent, it is not self-service. Publish both numbers where the platform team's leadership sees them weekly.

Do instead: pick the top three request types by volume and make them fully automatic, including approval. Approval is where these die — an automated pipeline gated on a human clicking approve has the latency of the human. Replace approval with policy where you can: pre-approve anything under a cost threshold, in a non-production environment, matching a known pattern. Reserve human review for the genuine exceptions, which will be a small minority.

4. The platform with no cost signal

Provisioning is easy, fast, and free at the point of use. Nobody sees a number. Within a year the platform accounts for most of the cloud bill and nobody can attribute it, because the platform provisions resources under its own identity with its own labels.

This one is insidious because it is a consequence of success. The platform made provisioning frictionless, which was the goal, and frictionless provisioning of things that cost money produces exactly what you would expect.

Do instead: the paved path must stamp ownership labels on everything it creates, no exceptions, and it must surface a cost estimate at request time. "This environment will cost approximately $340/month" in the provisioning flow changes behaviour measurably, and it costs a day to build. Retrofitting attribution after the fact is an order of magnitude harder — the resources exist, the ownership context is gone, and you are reverse-engineering it from CloudTrail. The allocation and showback machinery should be part of the platform's definition of done, not a later project.

5. Golden path with no escape hatch

The platform supports one way to build, one way to deploy, one runtime. Anything unusual is refused. The reasoning is sound — variance is what makes platforms unmaintainable.

The failure is that the teams with unusual requirements are disproportionately the ones running things that matter: the payments service with a compliance-mandated build process, the ML pipeline that needs GPU nodes and a 40 GB image, the legacy service that cannot be containerised this year. They are refused, they build their own, and now you have a shadow platform maintained by people who are not platform engineers, with none of your guardrails.

Do instead: tiered support with declared overrides, so off-path work is visible rather than hidden. Guardrails — policy, cost labels, network boundaries, audit — apply to everyone regardless of tier; only convenience and support are tiered. The full contract structure is in the piece on running a platform as a product.

6. The abstraction that leaks only during incidents

Day to day, the platform hides the infrastructure successfully. Then something breaks at 2 a.m., and the on-call engineer — who has never seen a raw manifest, does not know which node the pod is on, and cannot read the platform's generated network policy — has to debug through a layer they have no model of.

Abstractions that hide complexity during normal operation and expose it during failure are the worst possible arrangement, because they prevent the learning that would make the failure survivable.

Do instead: make the platform's output inspectable and make inspection part of routine work, not just incident work. A platform explain <service> command that prints the rendered manifests, the effective network policy, the node placement, and the resolved config — with provenance for each value — turns the abstraction into a lens rather than a wall. Then run a game day where teams debug through it, and fix whatever they could not see.

7. Building the platform before finding the second user

Eighteen months of development against a requirements document assembled from interviews, launched to an organisation that has since reorganised, adopted a new language, and moved half its workloads to a managed service.

The variant that is harder to spot: the platform is built for one team's needs, that team adopts enthusiastically, and the platform team declares success. The second team's requirements differ in ways that turn out to be structural, and the architecture cannot accommodate both.

Do instead: get to a second and third adopting team as fast as possible, on a deliberately narrow scope. Two dissimilar users at week six will reshape the architecture in ways that no amount of requirements gathering will. Ship something that does one job end to end for two teams rather than something that does six jobs partially for a hypothetical audience.

The meta-pattern. Six of these seven come from the same root: the platform team optimising for its own maintainability and control rather than for its users' outcomes. That is a rational instinct — the platform team is the one carrying the pager and the technical debt. But a platform nobody uses is maximally maintainable, and every one of these anti-patterns is a step along that gradient.

Diagnosing your own

Four questions, answerable this week:

  1. What fraction of production services created in the last quarter used the paved path? Not cumulative — last quarter only. Cumulative numbers hide a stalled trend.
  2. How many open platform tickets are requests to expose something the underlying API already supports? That count is a direct measure of anti-pattern one.
  3. Time from request to usable resource, p95, for your three most common request types. And what fraction completed with no human touching them.
  4. Can you produce a per-team cost breakdown of everything the platform provisioned last month, without a manual reconciliation?

Any of these being hard to answer is itself a finding. The teams with healthy platforms tend to have these numbers already, not because they are diligent about metrics, but because they have been treating adoption as the goal — and once you do that, these are simply the numbers you look at. The tooling choices that follow, including how much portability to build in, get much easier to reason about once the adoption question is settled.