Let’s be honest. How many hours have you spent Googling the “best way” to do something?
“What’s the best database for an e-commerce site?” “Should I use Microservices or a Monolith?” “What’s the ‘best practice’ for managing state in React?”
The software development ecosystem seems somewhat obsessed with the idea of “Best Practices.” They are sold as golden rules, infallible paths to clean, scalable, and easy-to-maintain software. In code reviews, in blog articles, in conferences... we are surrounded by “the best way” to build software.
But what if this search is, in fact, a trap?
Here’s the problem: when we apply these “rules” blindly, the result is often the opposite of what we expect…
Let’s be honest. How many hours have you spent Googling the “best way” to do something?
“What’s the best database for an e-commerce site?” “Should I use Microservices or a Monolith?” “What’s the ‘best practice’ for managing state in React?”
The software development ecosystem seems somewhat obsessed with the idea of “Best Practices.” They are sold as golden rules, infallible paths to clean, scalable, and easy-to-maintain software. In code reviews, in blog articles, in conferences... we are surrounded by “the best way” to build software.
But what if this search is, in fact, a trap?
Here’s the problem: when we apply these “rules” blindly, the result is often the opposite of what we expected. You implement microservices for a simple application and suddenly, you’re spending 80% of your time managing a complex deployment infrastructure instead of delivering value to the user.
Recently, I wrote about “The Cost of Perfection” (article in Portuguese) (overengineering). In it, I explored how our tendency to build “fortresses” trying to cover every possible future scenario often leads to complex, expensive, and hard-to-maintain systems.
What I realized is that overengineering is often a symptom. But what is the root cause? While reading the book “Software Architecture: The Hard Parts”, I was able to understand at least part of why this happens.
The book’s central insight is both brutal and liberating: true, efficient software architecture isn’t JUST about following rules or best practices. It’s about understanding, analyzing, and managing tradeoffs.
In this article, I want to share the main insights I took from this book, focusing on why we should stop looking for the “silver bullet” and start developing the most important skill of an architect: analyzing tradeoffs.
Breaking the Myth: Why “Best Practices” Can Be Dangerous
The biggest problem with the term “best practice” is that most of the time I see it being used, I also see it ignoring one of the most important words in software engineering (if not the most important): context.
A solution is never good or bad in a vacuum. It’s good or bad for a specific problem, for a specific team, at a specific time.
What is a “best practice” for Netflix, with its tens of thousands of microservices and an elite engineering culture, is almost certainly a terrible practice for a 5-person startup trying to validate a product idea (MVP) in the next 3 months.
Let’s take the most classic example from the last decade: Microservices.
If you listen to the conferences and read the most popular blogs, the “best practice” seems to be decomposing everything. The promise is tempting:
- Independent scalability!
- Autonomous teams!
- Deploy without fear!
But that’s only one side of the coin. What “Software Architecture: The Hard Parts” emphasizes is that this choice is not an improvement, it’s a tradeoff.
By choosing microservices, you are trading the development simplicity of a Monolith for massive operational and infrastructural complexity.
The Microservices Tradeoff:
- You GAIN: Granular scalability, independent deployment, fault isolation (maybe, in the best-case scenario with this architecture being well-implemented).
- You PAY WITH: Network complexity (latency!), data consistency (sagas, anyone?), difficulty in observability (what called what?), and the need for a much more mature DevOps culture.
The “best practice” of microservices is only “good” if the problem you have (e.g., difficulty scaling a specific part of your system) is greater than the new set of problems you will gain (e.g., managing a complex distributed architecture).
For 90% of applications at the beginning, the “cost” of microservices far outweighs the “gain.”
This is the danger: a “best practice” is just the solution someone found for a specific tradeoff. Copying the solution without understanding the original tradeoff is a recipe for disaster (or, at the very least, for a lot of overengineering, as we mentioned before).
Architecture as Tradeoff Analysis
If our job is no longer to blindly apply “best practices,” what is our function as developers and architects?
The book makes this very clear: our job is to analyze tradeoffs. It redefines software architecture in a powerful way. It ceases to be the search for a “perfect” or “correct” final state and becomes the continuous process of making hard decisions in a sea of ambiguities.
The most important part to understand is this: a tradeoff is not a choice between a “good” and a “bad” option. That would be easy. A tradeoff is almost always a choice between two (or more) options that are bad in different ways, or good in different ways.
A tradeoff is choosing between:
- Option A: Fast to develop, but hard to scale.
- Option B: Slow to develop, but easy to scale.
Which one is “right”? It depends. Do you need to launch fast (startup MVP) or do you need to support millions of users (established company)?
The book argues that the “hard parts” of architecture (like decomposing systems or managing distributed data) are hard precisely because there is no easy answer. All that exists are concessions.
The role of the architect or senior developer, therefore, changes drastically:
- From: “Knowing what the best solution is (e.g., Kubernetes).”
- To: “Knowing what the right questions to ask are (e.g., ‘What are the operational costs of adopting Kubernetes now vs. the cost of migrating to it later?’).”
The book teaches us that the architect is not the one with the magic answer. They are the one who deeply understands the business requirements and can map which architectural attributes (performance, maintainability, scalability, cost, etc.) are most important for that context and which can be sacrificed.
Three Practical Examples of Tradeoffs
Alright, the theory of “analyzing tradeoffs” is great. But what does it look like in day-to-day life? Let’s analyze some classic software engineering dilemmas through the lens of the book.
1. The Data Dilemma: Consistency vs. Availability
The question “SQL or NoSQL?” is perhaps the most misunderstood “best practice” of the last decade. At this moment, you are not facing a simple database choice; you are primarily facing a choice of consistency model.
- The “Best Practice” (The Myth): “NoSQL scales better!” or “SQL is ACID and reliable!”
- The Tradeoff Analysis: The real question is: What is more expensive for your business? Being temporarily inconsistent or being temporarily unavailable?
The Tradeoff (CAP Theorem):
- Choosing Strong Consistency (e.g., a traditional SQL database): You GAIN the guarantee that data is 100% correct and up-to-date across the entire system. A bank transaction always reflects the real balance.
The Cost (Tradeoff): You PAY with availability. In a distributed system, if the network fails between two nodes, the database must block the operation (become unavailable) to ensure consistency is not violated.
Choosing Availability (e.g., NoSQL with Eventual Consistency): You GAIN a system that always responds. A “like” on Instagram or an item added to a shopping cart works even if part of the system is slow.
The Cost (Tradeoff): You PAY with consistency. The system might take a few milliseconds (or seconds) for all nodes to agree. Your “like” might not appear to someone else immediately. You might sell a shopping cart item that just went out of stock (and you’ll have to deal with that later).
The Architect’s Job: It’s not to know if “SQL is better.” It’s to ask: “For this feature (e.g., shopping cart vs. payment), what is more critical: availability (the customer being able to use it) or consistency (the data being 100% correct right now)?”
2. The Communication Dilemma: Synchronous vs. Asynchronous
How should your services (or microservices) talk to each other?
- The “Best Practice” (The Myth): “Decouple everything! Use a Message Broker (Kafka/RabbitMQ) for everything!”
- The Tradeoff Analysis: Asynchronous communication is powerful, but it is incredibly complex.
The Tradeoff (Coupling):
- Choosing Synchronous (e.g., a direct REST API call): You GAIN simplicity and immediacy. You call the
ServiceB.DoThing()API and you know immediately if it worked or failed. The code is linear and easy to debug.The Cost (Tradeoff): You PAY with temporal coupling. If
ServiceBis slow or down, yourServiceAblocks along with it, waiting for a response. A failure cascades.Choosing Asynchronous (e.g., publishing a
ThingDoneevent): You GAIN resilience.ServiceAjust “shouts” the event and moves on. IfServiceBis down, it’s not affected.ServiceBwill process the event when it comes back up.The Cost (Tradeoff): You PAY with massive complexity. How do you monitor if the event was processed? How do you handle retries? What about the order of events? You’ve traded development simplicity for operational resilience.
The Architect’s Job: “For this workflow, what is more important: resilience (asynchronous) or simplicity of development and debugging (synchronous)?”
3. The Code Dilemma: Performance vs. Maintainability
How should you write that new module?
- The “Best Practice” (The Myth): “Clean Code! SOLID, DRY, KISS! Create all the right abstractions!”
- The Tradeoff Analysis: Every layer of abstraction that aids maintainability (“Clean Code”) has a performance cost.
The Tradeoff (Abstraction):
- Choosing Maintainability (Clean Code): You GAIN code that is easy to test, easy to change, and easy to understand (new devs thank you). You use Repositories, Use Cases, Dependency Injection, etc.
The Cost (Tradeoff): You PAY with performance (even if it’s small) and “boilerplate.” Each layer of abstraction adds overhead (more function calls, more memory allocation, more indirection).
Choosing Performance (“quick and dirty”): You GAIN raw speed. You write a “raw,” optimized SQL query that does 5 joins directly in your handler, skips the ORM, and returns the data in 10ms.
The Cost (Tradeoff): You PAY with maintainability. This code is fragile, hard to test, and a nightmare to change. If the database schema changes, good luck.
The Architect’s Job: “Is this a critical part of the system (hot path), like a game’s rendering loop, where every nanosecond counts? Or is it a ‘settings’ CRUD screen that’s used once a month, where the speed of modification is more important than the speed of execution?”
How to Start Thinking in Tradeoffs
From now on, “it depends” is your new favorite answer. But how can we turn this philosophy into a practical tool? How do you build this “tradeoff analysis” muscle?
Here are a few practical actions you can start applying today:
1. Change Your Vocabulary (and Your Questions)
The first step is the simplest and the most powerful. Stop asking “What’s the best?” and start asking “What are the costs?”.
Stop asking: “What’s the best database for this project?”
Start asking: “What are the tradeoffs of using PostgreSQL vs. MongoDB for this specific use case? What are we optimizing for? Query speed, infrastructure cost, or schema flexibility?”
Stop saying: “We should use microservices because it’s ‘best practice’.”
Start saying: “If we use microservices, we will gain X, but we will pay with complexity Y. Does this gain justify this cost... right now?”
2. Identify the Business “Drivers”
You can’t weigh a tradeoff if you don’t know what’s most important on the scale. Architecture must always serve the business goals.
Before any major technical decision, ask your team, PM, or leader: “What is the most important ‘driver’ for this feature?”
- Is it Speed of Delivery (Time-to-Market)? (Then maybe the “simpler” and faster solution to implement, even if it’s “ugly,” is the best one now.)
- Is it Extreme Performance? (Maybe that “Clean Code” with 5 layers of abstraction is a luxury we can’t afford here.)
- Is it Operational Cost? (Will using that managed AWS service be more expensive or cheaper than maintaining a Kubernetes cluster ourselves?)
- Is it Maintainability? (Is our team junior and in need of code that’s easy to understand, even if it’s slower?)
Only after you know what you’re optimizing for can you fairly analyze the tradeoff.
3. Use (and Learn to Like) ADRs: Architecture Decision Records
This is the most practical takeaway you can have. “Software Architecture: The Hard Parts” strongly advocates for documenting your decisions.
An ADR (Architecture Decision Record) is a simple, short Markdown document where your team records an important architectural decision. The basic format is:
- Title: “Choice of Communication between Service A and B”
- Context: “We need Service A to notify Service B when an order is paid.”
- Decision: “We have chosen to use Synchronous Communication (REST API) for now.”
- Consequences (The Tradeoff): “We gain simplicity in implementation and debugging. We accept the risk that if B fails, A will fail with it (temporal coupling). This is acceptable today because...”
- Options Considered (and why they were discarded): “We considered RabbitMQ, but the infrastructure cost and monitoring complexity were deemed too high for the current project phase.”
An ADR is the perfect antidote to the question every dev asks 6 months after something was built: “Why the hell did we do it this way?” It forces you to articulate and remember the whys behind the tradeoffs you analyzed at the time of the decision.
Conclusion: The Most Important Skill
For a long time, I measured my seniority by the number of “best practices” I knew. I thought the best architect was the guy who had the blueprint for the “right way” to build everything.
After gaining more experience, collecting some project scars (like the one I mentioned in my article on overengineering), my perspective changed completely. And the book “Software Architecture: The Hard Parts” was definitely one of the lenses that made me see the whys behind some of the challenges I faced (at least a part of them).
True seniority isn’t about having all the answers. It’s about understanding that there are no easy answers, only hard tradeoffs.
Software engineering is not an exact science like mathematics; it is a discipline of managing complexity and concessions. The “Myth of the Best Practice” gives us a false sense of security, while “Tradeoff Analysis” gives us the real tool to navigate real-world complexity.
So, the next time you find yourself in a debate about which technology is “better,” stop and change the question: “What are the tradeoffs we are making here?”
Thank you for reading and for your attention thus far. I would love to hear from you. Our community is built on real-world experiences.
What was the most difficult (or painful) tradeoff you’ve had to make on a project recently?
Leave your story in the comments!