How We Actually Do Architecture Without the Buzzwords
I've spent enough years watching teams over-engineer systems and under-engineer decisions to know that most architecture advice is just opinion dressed up as methodology. The real work happens in the messy middle where requirements change and nobody agreed on anything. Software Architecture In Practice isn't about drawing boxes and arrows until they look impressive in a design review. It's about making decisions that will matter in eighteen months when you're dealing with a system that's grown beyond what anyone planned for.
The Real Cost of Decisions Nobody Owns
Here's something most people miss when they start working in software architecture: the most expensive decisions aren't the technical ones. They're the organizational ones. Conway's Law isn't a clever observation. It's a prediction engine. I once inherited a distributed ordering system where three different teams owned separate microservices. Each team had their own deployment pipeline, their own API contracts, their own deployment schedules. The architecture looked clean on paper. In reality, a single customer order required nine synchronous HTTP calls across services, with each service having its own retry logic, its own timeout configuration, and its own idea of what failure looked like. The workaround wasn't architectural heroics. I just created a shared integration test suite that ran before any service could be deployed. If the contract changed between any two services, the deployment failed. This cut our integration breakage rate from roughly four incidents per week to maybe one per month. Took me about a week to set up. The team complained for two days and then accepted it.
The pattern here is boring and effective. Make breaking changes visible before they happen rather than after. Integration tests are not glamorous. They work.
Get the Full Details

When to Monolith and When Not To
People argue about monoliths versus microservices like it's a religious debate. In practice, the answer depends on how many people are working on the system and how independently those people need to deploy changes. If you have fewer than eight engineers touching the codebase and deployment cycles are measured in weeks rather than hours, a well-structured monolith is almost always the right call. The counter-intuitive part is that splitting a monolith into microservices before you have the organizational complexity to support them usually makes everything slower. You add network latency, distributed tracing overhead, deployment coordination, and data consistency problems. You gain nothing except the ability to deploy services independently, which doesn't matter if you only have two people deploying per week. I worked on a project where we split a perfectly fine monolith into twelve services because the CTO had read a blog post about how Spotify did it. Within six months, a simple feature request that used to take two days now took two weeks because someone had to coordinate changes across three services and deal with the eventual consistency issues between them. We spent another four months merging it back together.
Organize around bounded contexts. Use module boundaries inside a monolith to create logical separation. Only extract a service when you have a specific operational reason to do so. The reason should be deployment frequency, scaling requirements, or team independence. Not because it sounds modern.
Writing Architecture Decisions That Actually Get Used
Architecture decision records are one of those things that sound useful until everyone writes them in different formats and nobody reads them after the first week. I stopped treating them as documentation and started treating them as constraints. Each ADR should answer three questions: what decision was made, why was it made, and what assumptions are we still working with. If you can't write the assumptions section, you don't understand the decision well enough to make it. That's not a writing problem. It's a thinking problem. I keep a single file in the repository root. The file grows over time and gets harder to navigate. Instead of maintaining a separate Confluence page or a folder of individual documents, I keep everything in one place. People look at it when they're confused about why something was built a certain way. That's when the ADR matters. Not during the design phase when everyone pretends to read documentation.
The format I use is brutally simple. Decision identifier, status, context, decision, consequences. No prose. No narrative. Just the facts so someone reading it six months later doesn't have to guess why you chose Postgres over MongoDB for a workload that was clearly JSON-heavy.
The Database Question Nobody Asks Right
Most teams pick their database based on what their senior engineer knows or what their infrastructure team already runs. They should pick it based on query patterns, not personal preferences or existing knowledge. I once saw a team choose DynamoDB because they'd used it before and felt comfortable with it. Their actual workload involved complex joins across multiple entity types with reporting queries that needed aggregations across time ranges. DynamoDB handled the write throughput fine. The read patterns were painful. Every aggregation required a full table scan through Lambda, which meant cold starts, unpredictable latency, and bills that grew linearly with data volume. We switched to Postgres with a partitioned table strategy. The same queries became order-of-magnitude cheaper and faster. The team lost a week writing migration scripts. They gained something closer to sanity for the next two years.
If your primary access pattern is point lookups by ID with occasional range scans, a key-value store makes sense. If you need joins, complex filtering, or analytics, don't fight the database by trying to model relational data in a non-relational store. Just use a relational database.

Monitoring Without the Theater
Observability is another area where the industry has a habit of building elaborate dashboards that nobody looks at after the launch hype dies down. The practical approach is simpler and less impressive to present at architecture review meetings. Instrument the happy path first. Add structured logging with correlation IDs so a single request can be traced across services without needing a distributed tracing platform. Set alerts on business-level metrics, not just CPU utilization or memory usage. An error rate of zero means your error detection is broken, not that your system is healthy. I configured SLOs for our payment processing service based on actual customer impact rather than infrastructure metrics. The SLO was defined as "payment intent created to payment confirmed in under three seconds for ninety-five percent of requests." Everything else—latency percentiles, error budgets, deployment frequency—fed into that single measure. When engineering managers asked what success looked like, this was the answer.
The downside of SLO-based monitoring is that it requires you to actually understand your business constraints. You can't copy someone else's SLOs. They have to reflect real user expectations. Building that understanding takes time that most teams say they don't have. That's usually the wrong calculation.
When to Document and When to Just Talk
There's a sweet spot between over-documentation and under-communication. I've found that architecture decisions involving more than two teams should get written down. Decisions within a single small team are often better discussed in a five-minute conversation and summarized in a comment. The problem emerges when teams scale. A decision that made sense to three people becomes opaque to twelve people who weren't in the room. That's when documentation stops being optional. But the documentation should be minimal. A paragraph explaining the decision, a link to the ADR, and a note about what to contact if the rationale isn't clear. I've seen architecture diagrams become so detailed that they required maintenance teams to update them after every deployment. The diagrams became lies by omission rather than tools for understanding. Simpler is better. A single diagram showing service boundaries and data flow beats twenty diagrams showing every endpoint and every database table.

Software Architecture In Practice Means Living With Regret
Every architecture decision carries trade-offs. The goal isn't to eliminate regret. The goal is to make decisions you can live with and changeable enough to fix later if you're wrong. I once designed a caching layer that assumed read-heavy traffic with predictable access patterns. The product pivoted and the access patterns changed completely. The cache became a bottleneck rather than an acceleration. Rather than rewriting the caching layer, I added an abstraction boundary around it. The next iteration could swap implementations without changing the calling code. This took an extra week of development but saved us roughly three weeks of refactoring six months later. The pattern is to identify the decision point where you might need to change your mind. Build an interface or boundary at that point. The extra effort is small at the time and pays off whenever you're forced to reconsider.
You'll make mistakes. Systems will grow in directions you didn't predict. Teams will build things that contradict earlier decisions. This isn't failure. It's the normal state of software development. The architecture is the framework for managing that reality, not a blueprint for avoiding it. The best architecture decisions I've made are the ones I can revisit without rewriting everything. The worst ones are the ones locked in by process, politics, or inertia. Keep your options open. Document your reasoning. Be willing to change your mind when the evidence warrants it.