CONCEPT Cited by 1 source
Layering violation¶
Definition¶
A layering violation occurs when a component implemented at one layer of the system reaches into the responsibilities of another layer — typically downward into infrastructure concerns (threading, I/O, orchestration) that should belong to a lower layer. The visible symptom is that the component's "API" conflates its business logic with infrastructure details, and re-composing the business logic in a different infrastructure context is hard.
The canonical shape¶
Slack's Quip/Canvas frontend bundler before the refactor is a textbook example. From the post:
If we draw layers of capability from the operating system up to the application layer, we can see that the boundary of our builder is cutting across layers. It combines business logic, significant parts of a task orchestration framework, and parallelization. We really just wanted the top layer — the logic — so that we could re-compose it in a new orchestration context. We ended up with a work orchestrator (the builder) inside a work orchestrator (Bazel), and the two layers contending for slices of the same resource pool.
— Slack, Build better software to build software better
Visually:
┌──────────────────────────┐
│ Logic (build a bundle) │ ← what we wanted
├──────────────────────────┤
│ Orchestration │ ← the builder also did this
├──────────────────────────┤
│ Parallelization (workers)│ ← and this
├──────────────────────────┤
│ Language runtime │
├──────────────────────────┤
│ Operating system │
└──────────────────────────┘
The business-logic layer had pulled two layers below it into its implementation. The consequence was twofold:
- Re-composition was blocked. You couldn't use the builder's business logic in a different orchestration context, because the orchestration was baked in.
- Resource contention. Once Bazel was wrapping the builder, Bazel and the builder's internal worker pool were competing for the same CPU/RAM budget, and each was oblivious to the other's scheduling decisions.
Why layering violations happen¶
They are almost always the result of reasonable local decisions:
- At the time the builder was written, there was no outer orchestrator. Parallelizing inside the builder was the only lever to make it faster. The layering was fine in its original context.
- When an outer orchestrator arrives (Bazel, Ray, Kubernetes, Temporal), the previously-reasonable inner orchestration becomes a violation — but the violation is retrospective.
From Slack's account:
This strategy represented a set of reasonable trade-offs when it was written: it was a pragmatic attempt to speed up one piece of the build in the absence of a larger build framework. And it was faster than not parallelizing the same work!
The fix¶
The general fix is deleting the inner layer and letting the outer layer own the concern. In Slack's case:
- Delete the worker-pool code from the builder.
- Shrink the builder's API to one-bundle-in / one-bundle-out.
- Let Bazel parallelise across bundle builds by launching N instances of the simpler builder concurrently.
The result: the builder now has a single concern (business logic), Bazel has a single concern (orchestration and parallelism), and both layers can evolve independently.
See patterns/delete-inner-parallelization-inside-outer-orchestrator.
Other common layering-violation patterns¶
| Violation shape | Fix |
|---|---|
| Business logic doing HTTP retries | Push retries to an HTTP client/SDK layer |
| Application doing rate-limiting | Push to a service mesh / gateway |
| Services doing their own logging rotation | Push to the OS (logrotate) or agent |
| Data model embedding persistence details | Separate domain model from ORM entities |
| Test code doing its own parallelism | Push to the test runner |
| Worker doing its own leader-election | Push to an election service (Zookeeper/etcd) |
In every case, the pattern is: the violating component is doing something another existing layer already does better.
When the "violation" is actually right¶
Not every cross-layer reach is a violation. It's only a violation when (a) another layer genuinely owns the concern better and (b) pushing the concern there doesn't break performance.
Counter-examples — places where "violating" is right:
- Inlining hot-path code for performance (e.g. custom allocators) when the generic layer is too slow.
- Doing the work yourself when no outer orchestrator exists yet (Slack's original builder was in this regime — parallelising inside was the only option at the time).
- Adversarial boundaries (security, multi-tenancy) where trust in a lower layer is unsafe.
Related¶
- concepts/separation-of-concerns — the parent principle that layering violations break.
- concepts/service-coupling — the service-level cousin of layering violations.
- patterns/delete-inner-parallelization-inside-outer-orchestrator — the canonical fix at the orchestration altitude.
Seen in¶
- sources/2025-11-06-slack-build-better-software-to-build-software-better — canonical articulation of layering violation with a build- system example. Slack's frontend bundler combined business logic, orchestration, and parallelization in one component; the refactor deleted the orchestration and parallelization concerns and left only the business logic, yielding both maintainability and measurable speed-up.