Architecture is more important than clean code
I spent 2 weeks making my serverless functions clean. The architecture was wrong. I rewrote everything as a single Express server and it was simpler.
I spent 2 weeks refactoring a serverless setup. Every function was clean. Proper error handling, typed inputs, structured logging, unit tests.
It was still a mess.
The functions talked to each other through 4 different channels: SQS queues, S3 events, API Gateway, and direct Lambda invocation. A single user request touched 6 functions. Tracing a bug meant checking 6 different log groups. Deploying a change meant coordinating 3 services.
I rewrote everything as a single Express server. One process. One log stream. One deploy. The code was less “clean” (no separate functions, no event-driven patterns) but the system was dramatically simpler.
Architecture is more important than clean code. You can write perfect functions inside a wrong structure and still have a system nobody wants to touch.
Real examples where architecture mattered more than code quality
Example 1: The serverless monolith that wasn’t
We had 14 Lambda functions for a CRUD app. Each function was beautifully written. Typed interfaces, proper error boundaries, 90% test coverage.
The problem: every function shared a database connection pool through a global variable. Cold starts meant stale connections. A single slow function blocked the entire pool. Debugging meant correlating 14 CloudWatch log groups with different retention policies.
We merged them into one Express server. The code went from 14 clean functions to one 600-line file with inline handlers. It was “uglier” by every code quality metric. But cold starts disappeared, connection pooling worked normally, and debugging meant reading one log stream.
The “clean” version took 45 minutes to debug a production issue. The “ugly” version took 5.
Example 2: The microservice that served 12 users
A project I consulted on had 3 microservices: auth, API, and notifications. Each had its own repo, its own CI pipeline, its own database.
The product had 12 active users. A single developer maintained all 3 services.
Deploying a feature that touched all 3 services took 2 days: update auth, deploy, update API, deploy, update notifications, deploy. Each deploy had to be sequenced correctly or the system broke. Rollbacks meant rolling back 3 services in reverse order.
We merged them into a monolith. One repo, one deploy, one database. The code was less modular. The boundaries between “services” were just folders instead of network calls. But deploying a feature went from 2 days to 10 minutes.
Example 3: The event-driven system with 2 events
An e-commerce side project used an event-driven architecture. Every action published events. Order created, payment received, email sent, inventory updated.
The problem: the “event-driven” part was 300 lines of event bus code for 4 events that always happened in sequence. Order created → payment received → email sent. They were never independent. They were never consumed by more than one handler.
The event bus added latency (each hop was 50-200ms), complexity (event schemas, retry logic, dead-letter queues), and opacity (tracing a payment meant following events across 3 services).
We replaced it with synchronous function calls. The checkout flow went from 4 events across 3 services to one function that called 3 helpers. Latency dropped by 600ms. The code was easier to read top-to-bottom.
Example 4: The database per service that shared data anyway
A team I worked with followed the “database per service” pattern strictly. User service had its own DB. Order service had its own DB. Notification service had its own DB.
But orders needed user data. Notifications needed order data. So they built API calls between services to fetch the data they needed. A single page load triggered 3 internal API calls to assemble data that would have been one JOIN.
The “proper” architecture meant 3 databases, 3 connection pools, 3 backup strategies, and network calls that used to be JOINs. A simple “show me my orders with user details” query was now a distributed systems problem.
We consolidated to one database with proper schema separation. The code was “less clean” (one DB instead of 3) but the queries were simpler, backups were simpler, and page loads were 3x faster.
Example 5: The k3s cluster running one blog
I run my blog on k3s. One pod, one container, one NodePort service. The blog gets maybe 50 visits a day.
I could have made it “proper”: multi-replica deployment, ingress controller, cert-manager for HTTPS, horizontal pod autoscaler, service mesh for observability.
Instead, it’s one Caddy container serving static files from a PVC. Deploy is npm run build + rollout restart. If it goes down, I restart the pod. Total infrastructure: 3 YAML files.
The “proper” setup would have taken a day to configure and would require ongoing maintenance. The simple setup took 20 minutes and has run for months without intervention.
The pattern
Every example above follows the same arc:
- Choose an architecture because it’s “the right way” (serverless, microservices, event-driven, DB-per-service, Kubernetes)
- Write clean code within that architecture
- Spend all your time fighting the architecture instead of shipping features
- Simplify the architecture
- The code gets “less clean” but the system gets dramatically easier to work with
When “clean code” can’t save you
Clean code practices (small functions, typed interfaces, test coverage, error handling) operate at the function level. They make individual pieces easier to understand.
But they don’t help when:
- A request touches 6 services and you can’t trace it
- Deploying a change requires coordinating 3 repos in sequence
- A simple query requires 4 API calls because each service owns its data
- Cold starts add 3 seconds to every request because your functions share a connection pool
- You spend more time on infrastructure than on features
These are architecture problems. No amount of clean functions fixes them.
What I actually do now
I start with the simplest architecture that could possibly work:
- One process until you have a reason to split
- One database until you have a reason to separate
- Synchronous calls until you have a reason to add events
- One repo until you have a reason to split
- One server until you have a reason to orchestrate
Then I add complexity only when I hit a real constraint:
- “This function needs to scale independently” → split it
- “This data is accessed by multiple services” → separate the DB
- “This operation takes too long and blocks the response” → make it async
- “This team can’t work independently with one repo” → split it
The key word is “real.” Not “might need someday.” Not “best practice says.” Real, current, measured constraint.
The bottom line
Clean code is a local optimization. Architecture is a global one. You can have perfect functions inside a wrong structure and still have a system that’s painful to deploy, debug, and change.
Start simple. Add architecture when you have evidence you need it. The best architecture is the one that makes the entire system easy to understand, not the one that makes each piece look good in isolation.