Building With Intent, Not Just Tools
Why the choice of tools matters less than the intent behind how you use them, and how to maintain architectural clarity regardless of the technology stack.
Context
The technology industry has a tool fixation. "What stack do you use?" is often the first question about any project. Blog posts announce technology choices as if they are the most important architectural decision. Teams spend weeks evaluating frameworks and databases, then spend months building without a clear architectural intent.
Tools matter. But intent matters more. A well-designed system built with mediocre tools will outperform a poorly designed system built with best-in-class tools. The intent, the clear understanding of what the system needs to do, how it needs to fail, and how it needs to evolve, is what separates systems that work from systems that merely run.
What Intent Means
Architectural intent is the set of explicit decisions about what the system optimizes for and what it trades away. It answers questions like:
- What is the primary concern: throughput, latency, consistency, availability, or cost?
- What failure modes are acceptable and which are not?
- What will change frequently and what will remain stable?
- Who will maintain this system and what is their expertise level?
- What is the expected lifetime of this system?
A system built with clear intent makes these trade-offs explicitly. A system built without intent makes them accidentally, through defaults, copy-pasted configurations, and whatever the framework happens to do out of the box.
The Tool Trap
The tool trap manifests in several ways:
Choosing Tools Before Understanding Requirements
See also: Choosing Boring Technology on Purpose.
A team decides to use Kubernetes before determining whether their workload needs container orchestration. They adopt a service mesh before having enough services to justify one. They choose a time-series database before understanding their query patterns.
The result: complexity that serves the tool's design goals rather than the system's design goals.
Letting the Tool Dictate the Architecture
Every tool has opinions. Frameworks have conventions. Databases have access patterns they are optimized for. When you adopt a tool without intent, the tool's opinions become your architecture.
This is not inherently bad. Framework conventions exist because they represent accumulated wisdom. But they represent accumulated wisdom for a general case, not for your specific case. When your case diverges from the general case, following the tool's opinions produces a system that is optimized for the wrong thing.
Related: Engineering Decisions That Reduce Pager Fatigue.
Confusing Tool Proficiency With Engineering Skill
Knowing React deeply is not the same as knowing how to build good user interfaces. Knowing Kafka deeply is not the same as knowing how to design good event-driven systems. Tool proficiency is valuable, but it is a subset of engineering skill.
The most effective engineers I have worked with can articulate their architectural intent clearly, then select tools that serve that intent. The tools are interchangeable. The intent is not.
Building With Intent in Practice
Step 1: Define the Core Invariants
Before selecting any tools, define the invariants that the system must maintain. These are the properties that, if violated, mean the system is broken.
Examples:
- Every payment must be processed exactly once.
- User data must be encrypted at rest and in transit.
- Search results must reflect data no older than 30 seconds.
- The system must remain available for reads even if writes fail.
These invariants drive architectural decisions independent of tools. Whether you use PostgreSQL or DynamoDB, the "exactly once" invariant requires idempotency handling.
Step 2: Identify the Primary Tension
Every system has a primary tension: two desirable properties that are in conflict. Identifying this tension early focuses the design.
Common tensions:
| Tension | Example |
|---|---|
| Consistency vs. availability | Payment system that must be correct vs. always available |
| Latency vs. thoroughness | Search that must be fast vs. comprehensive |
| Flexibility vs. simplicity | Configuration system that must be powerful vs. easy to use |
| Cost vs. performance | System that must be cheap vs. fast |
| Security vs. usability | Authentication that must be secure vs. frictionless |
The intent is to resolve this tension explicitly rather than accidentally. "We prioritize consistency over availability for payment operations and availability over consistency for product catalog reads" is an intent statement. It drives different design choices for different parts of the system.
Step 3: Design the Failure Modes
Most tool selection focuses on the happy path: how the system works when everything is working. Intent-driven design starts with the failure modes: how the system should behave when things go wrong.
For each failure mode, define:
- What the user should see
- What the system should log
- What automatic recovery should occur
- What human intervention is required
These decisions are more important than which database or which message queue you use. A system that handles failures well but uses a suboptimal database will serve users better than a system that uses the optimal database but crashes on every edge case.
Step 4: Select Tools That Serve the Intent
Now, with invariants defined, tensions resolved, and failure modes designed, select tools. The selection criteria are specific:
- Does the tool support the invariants naturally, or do I need to work around it?
- Does the tool's default behavior align with my resolved tensions, or fight them?
- Does the tool's failure mode match my designed failure mode, or introduce unexpected ones?
- Can my team operate this tool effectively?
This is a different evaluation than "which tool is best" in the abstract. It is "which tool is best for this specific intent."
The Intent Document
For significant systems, I write a short intent document before any design work. It covers:
- Purpose: What does this system exist to do? (One paragraph)
- Core invariants: What must always be true? (Bulleted list)
- Primary tension: What trade-off defines this system? (One sentence)
- Acceptable failure modes: What can go wrong, and how should the system respond? (Table)
- Non-goals: What is this system explicitly not trying to do? (Bulleted list)
This document is typically one page. It is not a design document. It is the foundation that the design document builds on. When design decisions conflict, the intent document resolves them.
When Tools Do Matter
Tools are not irrelevant. They matter when:
- Operational maturity: A tool your team knows how to operate is better than a tool that is theoretically superior but operationally unfamiliar.
- Ecosystem support: A tool with good libraries, documentation, and community support reduces development friction.
- Performance characteristics: When the workload has specific performance requirements that only certain tools can meet.
- Compliance requirements: When regulatory requirements mandate specific capabilities (encryption, audit logging, data residency).
But these are constraints on tool selection, not substitutes for intent. The intent determines what you need. The tool constraints determine which options are available.
Key Takeaways
- Architectural intent (what the system optimizes for and trades away) matters more than tool selection.
- Choosing tools before understanding requirements leads to complexity that serves the tool, not the system.
- Define core invariants, identify the primary tension, and design failure modes before selecting tools.
- An intent document (one page covering purpose, invariants, tensions, failure modes, and non-goals) provides a foundation for all design decisions.
- Tools matter for operational maturity, ecosystem support, and specific performance or compliance needs, but these are constraints, not drivers.
- The most effective engineers articulate intent clearly and then select tools to serve it. The tools are interchangeable. The intent is not.
Further Reading
- Building a Rate-Limited API From Scratch: Implementing token bucket and sliding window rate limiting with Redis, including burst handling, multi-tier limits, and measured overhead.
- Building a Simple Search Index: Designing an inverted index from scratch with tokenization, ranking, and query parsing, then comparing it against Postgres full-text search.
- Building a View Counter System With Postgres: Designing a page view counter that handles concurrent writes, avoids double-counting, and stays responsive under load using Postgres.
Final Thoughts
The question "what tools should I use?" is almost always the wrong first question. The right first question is "what am I building, and what properties must it have?" The answer to the second question narrows the tool selection naturally. More importantly, it produces a system with coherent design decisions that reinforce each other, rather than a collection of tools that happen to run together. Build with intent. The tools will follow.
Recommended
Designing an Offline-First Sync Engine for Mobile Apps
A deep dive into building a reliable sync engine that keeps mobile apps functional without connectivity, covering conflict resolution, queue management, and real-world trade-offs.
Jetpack Compose Recomposition: A Deep Dive
A detailed look at how Compose recomposition works under the hood, what triggers it, how the slot table tracks state, and how to control it in production apps.
Event Tracking System Design for Android Applications
A systems-level breakdown of designing an event tracking system for Android, covering batching, schema enforcement, local persistence, and delivery guarantees.