Customer Obsessed Engineering

Customer Obsessed Engineering

If you’re not using “declarative teams," you have only an illusion of control

Shifting from "how to do it" to "what’s the outcome" transforms your productivity, reduces complexity, and makes your systems reliable.

Zac Beckman
Aug 31, 2025
∙ Paid
Photo by James Lee on Unsplash

The most productive teams I’ve worked with never argue about implementation details.

That’s probably surprising if you’ve spent years in rooms with engineers propounding about the merits of different algorithms, frameworks, or architectural patterns. What I’ve learned is that the teams who spend their time arguing about how to build something are the same teams that consistently miss deadlines, accumulate technical debt, and burn out their best people.

The highest-performing teams focus on what they want to achieve, then let declarative systems handle the how.

This isn’t just about programming paradigm or tool selection. It’s a fundamental change in how we think about building software, managing teams, and delivering customer value.

It’s the difference between organizations that scale effortlessly and those that collapse under their own complexity.

The hidden cost of procedural thinking

Most of us were trained to think procedurally. We write step-by-step instructions. “First, do this, then do that, and if this happens, handle it this way.” It feels natural because that’s how we’ve been taught to solve problems since childhood.

But procedural thinking carries a hidden tax on everything we build.

Here’s an example I bet sounds familiar: your team finds themselves inheriting deployment tooling for a complex product. I ran into this recently, to the tune of well over 1,000 lines of disparate scripts, loosely tied together across different systems. It handled environment setup, dependency installation, configuration management, and service startup. The scripts had been lovingly maintained by just one or two engineers for years. And it worked really well — most of the time.

The problem wasn’t that the script failed (it did, in spectacular ways, often completely derailing a new product release). The problem was what that script represented: years of institutional knowledge understood by one or two people, and requiring constant maintenance as the underlying infrastructure evolved (which was pretty often).

If you’re new, welcome to Customer Obsessed Engineering! Every week I publish a new article, direct to your mailbox if you’re a subscriber. As a free subscriber you can read about half of every article, plus all of my free articles.

Anytime you’d like to read more, you can upgrade to a paid subscription.

After those two people left the company, deployment became a bottleneck. When AWS updated their base images, someone had to trace through the script line by line to figure out what broke. When the team wanted to deploy to a different region, they discovered hardcoded assumptions baked into dozens of conditional statements. Little incompatibilities or unforeseen eventualities that the script just couldn’t handle. Sometimes it took days to reverse engineer a fix.

Here’s my key insight about this: every procedural system you build becomes a maintenance burden that grows exponentially with complexity.

Declarative systems are simple. They describe a clear end-state, not a series of complex instructions that hopefully get you there. Consequently, they become more valuable as they scale.

This is the fundamental difference between telling a system how to do something versus telling it what you want accomplished.

What “declarative” really means

Declarative programming isn’t just about using Infrastructure as Code (“IaC”) instead of writing for-loops and if-then statements (though that’s part of it). It’s about expressing intent rather than implementation.

When I tell Kubernetes “I want three replicas of this service running with these resource constraints,” I’m being declarative. The system figures out how to make that happen, how to recover when nodes fail, how to scale when load increases. I describe the desired state, the “what I want,” not the “how.” The system maintains it, figuring out the “how” for me.

CSS is declarative: “I want this text to be bold, pulsing dark blue on a midnight background.” It describes what I want to appear on my web page. The browser handles the complex rendering pipeline, layout algorithms, and cross-platform compatibility.

But “being declarative” is also about thinking intentionally, a way to reason about systems so they mirror my intention. By “systems,” I don’t mean code, I mean organizational systems. How your team operates. How you design your architecture, your product, your business.

That’s what most people miss: declarative thinking applies far beyond code.

When I create a Definition of Done that specifies the characteristics of completed work, I’m being declarative about quality standards. When I establish value stream maps that define customer outcomes, I’m being declarative about business objectives. When I build a playbook that specifies artifacts and quality gates for each delivery phase, I’m being declarative about process.

The teams that master this distinction operate at a fundamentally different level than their peers.

In using steel threads, I discovered that successful teams naturally gravitate toward declarative approaches. They don’t argue about how to implement the “right to be forgotten” for GDPR compliance — they declare what needs to happen and let the architecture emerge from that constraint.

Those declarations became architectural constraints guiding every decision. Teams stop bikeshedding about microservice boundaries and started building systems that provably meet requirements.

How procedural programming fails at scale

Procedural systems fail for the same reason that manually managing server configurations fail: they don’t account for the complexity explosion from system growth.

Every time you write a procedure, you’re making assumptions about the environment, the inputs, the available resources and the desired outcomes. You rely on expected side effects, assumptions about what’s happening outside the procedure. Those assumptions become technical debt the moment any condition changes.

I’ve seen this pattern repeatedly:

  1. In DevOps: you start with simple deployment scripts, but as requirements evolve, scripts grow more complex. Eventually, it’s an unmanageable forest of conditional logic that nobody wants to touch.

  2. With code complexity: systems designed procedurally become tightly coupled. When one piece changes, ripple effects cascade. What should be a simple update becomes a multi-team coordination effort.

  3. During testing: procedural code demands testing every possible execution path, every external dependency. Side effects cause complications. Teams must choose between inadequate tests or spending too much time maintaining those tests.

  4. With knowledge bottlenecks: procedural systems distill complex knowledge into internal implementation detail. When the code author departs so does the knowledge. It takes months to reverse-engineer implicit, complex logic.

The declarative alternative

Declarative systems solve these problems. Instead of encoding how something should happen (which brings with it all the fragility of a procedural approach), you specify what should be true, then rely on the system to maintain those conditions.

There are a few arguments against declarative code that I hear now and then. In my experience these are based on misinformation:

“Declarative code is hard to think about.”

I couldn’t disagree more. Declarative systems are concise, clear, and consolidate all of your logic in a single place. It’s easier to comprehend, and far easier to test. This is really about comfort and changing how we were taught to solve problems — we’re more comfortable thinking procedurally.

“You lose control with declarative approaches.”

You gain control. Instead of controlling implementation details, you control outcomes. When you declare “this service should always have three healthy replicas,” the system ensures that’s true even when nodes fail, traffic spikes, or deployments occur.

“Declarative systems are harder to debug.”

Procedural systems require tracing through execution paths to understand what went wrong. Declarative systems let you compare current state to desired state. The difference between current and desired state is the bug.

When a Kubernetes deployment fails, you don’t debug the container orchestration logic. You check if your declared resources can be satisfied and fix the mismatch.

Remember that deployment script example above? 1,000-line deployment script that sometimes failed spectacularly and required days of diagnosis?

We replaced it with IaC in Terraform, declaring the desired infrastructure state. The declarative specification is just a few hundred lines long. More importantly, we now deploy to any region. We change environment settings with a few variables, not by rewrite procedures. Deployments that previously took days (or, just outright failed) are now done with a click, via a dashboard, and it’s utterly reliable.

I mentioned that declarative thinking applies far beyond code. So how do we “scale up” from declarative code to declarative thinking?

I’d really appreciate it you could refer a friend. Plus, three referrals and you earn a free month!

Refer a friend

Building declarative teams

The most profound realization is that declarative thinking extends beyond code to how we structure teams and processes.

Traditional project management is procedural: detailed project plans with dependencies, specific task assignments, and rigid timelines. When requirements change (and they always do), the entire plan requires revision.

Declarative project management focuses on outcomes: “We need a user authentication system that handles 100,000 concurrent users with 99.9% uptime.” The team figures out how to deliver that outcome, adapting their approach as they learn more about the problem space.

Goals and emphasis for the team shift away from activities and toward specific, measurable, provable outcomes.

This is exactly what the Delivery Playbook enables. Instead of prescriptive methodologies that tell teams how to work, the playbook declares what good delivery looks like, providing a framework to achieve it reliably.

Here’s a practical framework for moving your team toward declarative thinking:

  1. Start with outcomes, not activities. Instead of “implement user authentication,” declare “users can securely access their data from any device.” The first focuses on building something; the second focuses on enabling something. This is the shift away from procedural thinking. It takes time, but with practice you’ll get there.

  2. Express constraints, not solutions. Instead of “use microservices with REST APIs,” declare “components must be independently deployable and testable.” Let the architecture emerge from the constraints.

  3. Define interfaces, not implementations. Instead of detailed API documentation explaining how to use your service, create OpenAPI specifications that declare what your service provides. Generate the documentation, client APIs, and validation from the specifications and code.

  4. Build systems that maintain state. Instead of scripts that perform actions, build systems that ensure conditions remain true. Think about Kubernetes’ declarative nature: “there must always be 3 healthy replicas running.” Use Terraform, Kubernetes and frameworks like Ash Framework.

  5. Test behaviors, not code paths. Instead of unit testing internal implementation details, write tests that assert observable behaviors and outcomes. Focus on policies, for instance, “forbid non-administrators from creating a new account.”

  6. Use design thinking. Build organization processes that model your declarative “way of working.” Adopt inputs and specific outputs for project phases, relying on statements about target state (not focusing on “what activities everyone should be doing”). The playbook is a great example to use as a model.

  7. Only measure progress. Focus on evidence-based delivery. What matters is achieving the declared target state. Progress is measured only by validating that your target state — your feature — is working and deployed.

Deep dive: a declarative security API by contract

I’ve written a lot about declarative thinking in abstract. An example will be helpful to crystalize what it really means. Let’s say we’re building a project tracking system for our agile team. We want to manage sprints and their activities.

One thing we’ll need is a way to track those sprints. Here’s one approach — a fairly traditional, procedural one:

Keep reading with a 7-day free trial

Subscribe to Customer Obsessed Engineering to keep reading this post and get 7 days of free access to the full post archives.

Already a paid subscriber? Sign in
© 2025 Boss Logic LLC · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture