The API Worked. The Architecture Didn’t.
A practical look at why a working API does not always mean a healthy architecture, covering coupling, synchronous work, database bottlenecks, caching, microservices, failure modes, and the importance of clear boundaries.
Backend
Tip
Sep 19, 2026 · 10 min read
Your API returns 200 OK.
The tests are green.
The frontend works.
Everything looks fine.
Until you need to change something.
Then suddenly, a small feature requires changes in five places, one external service is timing out, the database is doing far more work than expected, and a simple request has become responsible for half of the system.
That is when you realize something important:
A working API does not necessarily mean a healthy architecture.
The Endpoint Looks Fine
Consider a simple order endpoint:
POST /orders/
The request creates an order and returns the result.
Nothing unusual.
But inside the endpoint, you might eventually find something like this:
def create_order(request):
order = create_order_in_database(request.data)
update_inventory(order)
send_confirmation_email(order)
notify_warehouse(order)
update_customer_statistics(order)
return Response(order)
It works.
The endpoint is easy to understand.
The tests pass.
But now the request depends on almost everything:
- the database
- inventory logic
- email delivery
- warehouse notifications
- customer statistics
The API is no longer just creating an order.
It has become the entry point for a large part of the business process.
That is where architectural problems usually begin.
Correctness Is Not the Same as Architecture
There are several different questions we can ask about a backend system.
Does it work?
Does the endpoint return the expected result?
Is it correct?
Does it preserve the business rules and data integrity?
Is it reliable?
What happens when another dependency fails?
Is it scalable?
What happens when traffic increases ten or one hundred times?
Is it maintainable?
Can another engineer safely change it six months from now?
An API can be completely correct while still having serious architectural problems.
This distinction is important because many systems are evaluated only by their happy path.
Request → API → Database → Response
If that works, the feature is considered finished.
But real systems rarely live on the happy path.
One Request Can Do Too Much
One of the most common problems in backend systems is putting too much work inside the request lifecycle.
For example:
HTTP Request
│
▼
Create Order
│
├── Save Order
├── Update Inventory
├── Generate Invoice
├── Send Email
├── Send SMS
├── Notify Warehouse
└── Update Statistics
│
▼
HTTP Response
The problem is not that these operations exist.
The problem is that they are all coupled to one request.
If sending an email takes three seconds, the API waits three seconds.
If the SMS provider is unavailable, the API may fail.
If warehouse notification times out, the client may receive an error even though the order was already created.
Now you have a much more difficult problem:
What exactly succeeded?
Failure Makes It More Complicated
Imagine this sequence:
1. Create order ✓
2. Update inventory ✓
3. Send email ✓
4. Notify warehouse ✗
The database says the order exists.
The customer may have received an email.
The warehouse did not receive its notification.
The API returns an error.
The client retries the request.
Now what happens?
Do you create another order?
Do you send another email?
Do you update the inventory twice?
This is where concepts like idempotency, transactions, retries, outbox patterns, and background jobs become architectural concerns rather than implementation details.
A system should not only define what happens when everything works.
It should define what happens when only part of the system works.
The Synchronous Everything Problem
Synchronous code is not inherently bad.
For many operations, it is exactly what you want.
The problem starts when every operation becomes synchronous simply because it is easier to implement.
For example:
Request
│
├── Database
├── Payment Provider
├── Email Provider
├── SMS Provider
├── Analytics
└── Notification Service
│
▼
Response
The API response time is now influenced by every dependency.
Instead, some work may naturally belong outside the request lifecycle:
Request
│
▼
Database
│
▼
Response
Background Jobs
├── Email
├── SMS
├── Notifications
└── Analytics
This does not mean everything should become asynchronous.
It means the architecture should distinguish between:
Work required to complete the request
and
Work that can happen after the request succeeds.
That distinction alone can make a large difference in reliability and performance.
The Database Can Hide Problems Too
Architectural problems are not always visible in the API code.
Sometimes the endpoint looks clean while the database is doing unreasonable work.
For example:
orders = Order.objects.all()
for order in orders:
print(order.customer.name)
It may work perfectly with 20 records.
With thousands of records, you may discover an N+1 query problem.
Other common examples include:
- missing indexes
- unbounded queries
- unnecessary joins
- large database scans
- loading entire tables into memory
- expensive aggregations on every request
- unnecessary serialization
- selecting fields that are never used
The API contract does not tell you how expensive the operation is internally.
A simple endpoint can hide an expensive system.
“We’ll Add Caching Later”
Caching is another common architectural reaction.
The application becomes slow, so Redis is introduced.
The endpoint becomes faster.
Problem solved.
Sometimes.
But caching can also hide the real problem.
If a query is slow because the database lacks the right index, caching may reduce the symptoms without fixing the underlying issue.
If an API is making ten unnecessary queries, caching may make those requests less painful without addressing why the application needs ten queries in the first place.
Caching is useful when it solves a real workload problem.
It should not automatically become the first response to every performance problem.
A useful question is:
What became expensive, and why?
Only then should you decide whether caching is the right solution.
Microservices Don’t Automatically Fix Architecture
Another common assumption is that a large application has an architectural problem, so it should be split into microservices.
But moving code into separate services does not automatically create better boundaries.
You can easily build this:
Service A → Service B → Service C → Service D
↑ ↓ ↓
└──────────┴───────────┘
Now a simple operation depends on multiple network calls.
You have introduced:
- network failures
- timeouts
- retries
- distributed tracing
- service discovery
- deployment coordination
- data consistency problems
- more operational complexity
The system may now be distributed without actually being decoupled.
That is a distributed monolith.
The important question is not:
“How many services do we have?”
It is:
“Where are our actual boundaries?”
Architecture Is About Boundaries
Good architecture is not primarily about frameworks or infrastructure.
It is about boundaries.
A typical backend might have boundaries such as:
HTTP
│
▼
API Layer
│
▼
Application Layer
│
▼
Domain Logic
│
▼
Persistence / Infrastructure
Each layer should have a reason to exist.
The API layer should not need to know how every external integration works.
Business logic should not depend unnecessarily on HTTP details.
Infrastructure concerns should not leak everywhere through the application.
The exact structure will differ between projects.
A small Django application does not need an enterprise architecture diagram with twenty layers.
The goal is not maximum abstraction.
The goal is intentional coupling.
Ask What Happens When Things Fail
One of the easiest ways to discover architectural problems is to stop asking only about successful requests.
Ask questions like:
- What happens if PostgreSQL becomes slow?
- What happens if Redis is unavailable?
- What happens if an external API times out?
- What happens if the client retries the same request?
- What happens if a background worker crashes?
- What happens if a message is delivered twice?
- What happens if a deployment happens halfway through a workflow?
- What happens if one dependency is temporarily unavailable?
These questions often reveal more about your architecture than looking at the happy path.
A mature backend is not one where failures never happen.
It is one where failures have predictable consequences.
The API Is Only the Surface
An API is an interface.
It tells clients how to communicate with your system.
But the API contract does not tell you whether the architecture underneath is healthy.
Two endpoints can have exactly the same contract:
POST /orders/
One might execute three well-defined operations with clear boundaries.
Another might trigger fifteen tightly coupled operations across multiple systems.
From the frontend's perspective, both return:
{
"id": 123,
"status": "created"
}
The difference only becomes visible when the system needs to handle scale, failures, retries, or change.
That is why API design and architecture should not be treated as the same problem.
A Better Definition of “It Works”
For backend systems, “it works” should mean more than:
Request → 200 OK
A stronger definition is:
Happy path
+
Expected failures
+
Retries
+
Concurrency
+
Growth
+
Future changes
You do not need to solve every possible failure before shipping a feature.
That would be unrealistic.
But you should understand the important failure modes of the system you are building.
Especially the ones that can cause:
- duplicate data
- lost events
- inconsistent state
- cascading failures
- unexpected costs
- long response times
- difficult recovery
Keep the Architecture Boring
Good backend architecture is often surprisingly boring.
A database.
A web application.
A queue when background work is actually needed.
A cache when caching provides measurable value.
A few clear modules.
Simple boundaries.
Predictable failure handling.
There is no requirement to use every interesting technology.
You do not need Kafka because your application has events.
You do not need Kubernetes because you have containers.
You do not need microservices because the codebase is getting larger.
You do not need Redis because an endpoint is slow.
The architecture should evolve because the requirements evolve.
Not because a technology exists.
The Real Test
There is a simple test I like for architecture:
How difficult is the next change?
Suppose you need to add a new notification provider.
If that requires modifying the order endpoint, customer model, payment service, warehouse integration, and several unrelated tests, your boundaries may be too tightly coupled.
If you can add it in one well-defined place without touching unrelated parts of the system, the architecture is giving you leverage.
This is one of the biggest differences between code that merely works and a system that can keep evolving.
Good architecture makes future changes cheaper.
Not every change will be easy.
But the system should not make simple changes unnecessarily difficult.
Final Thought
A working API is a feature.
A healthy architecture is what allows that feature to survive change, failure, and growth.
The goal is not to build the most sophisticated architecture.
The goal is to build a system where responsibilities are clear, failures are understood, dependencies are intentional, and the next engineer can change the code without being afraid of what will break.
Because eventually, every API gets tested by something more difficult than a request.
Change.
Related Notes
When Redis Is Not the Solution
Redis is fast and extremely useful, but that doesn't make it the right solution for every backend problem. A practical look at when Redis helps with caching, rate limiting, OTPs, locks, queues, sessions, and temporary data—and when it simply adds unnecessary infrastructure and complexity.
Django Transactions: What atomic() Actually Protects
Learn how Django transactions work, what transaction.atomic() actually protects, and how to handle race conditions, row locking, database constraints, nested transactions, and post-commit side effects with select_for_update() and on_commit().
The Hidden Cost of Django REST Framework Serializers
Learn how Django REST Framework serializers can cause N+1 queries, slow API responses, and unnecessary database work—and how to optimize them with select_related, prefetch_related, annotations, and better serializer design.
Building a Django Package — Part 4: Publishing django-healthkit to PyPI
In this final part, we prepare django-healthkit for release, build and validate the package, test it on TestPyPI, publish it to PyPI, and create a Git tag and GitHub release.
Building a Django Package — Part 3: Health Check Manager and Endpoint
In Part 3, we connect the database and cache health checks, build the health check manager, expose a Django health endpoint, measure check latency, and return a structured health status.
Building a Django Package — Part 2: Database and Cache Health Checks
In this part, we implement the first health checks for django-healthkit, covering database connectivity and Django cache functionality with simple, independent, and testable checks.
Building a Django Package — Part 1: Setting Up django-healthkit
In this part, we build the initial structure of django-healthkit, configure the package with pyproject.toml, and prepare it for development.
Building a Secure Webhook Receiver for Server-to-Server Communication
How I built a secure FastAPI webhook receiver using RSA signatures to enable authenticated server-to-server communication, proxy requests, and connect applications across different network environments.
Why I Switched to Conventional Git Commit Messages
Why I switched from inconsistent Git commit messages to a Conventional Commits style, with practical examples for cleaner and more maintainable Git history.
Building django-healthkit
How a simple health endpoint for Bidar turned into django-healthkit, a lightweight Django health-check package.
Designing a Gold Jewelry E-Commerce Database with Django
Learn how to design a scalable Django database for a gold jewelry e-commerce platform by modeling products, attributes, and purchasable product variants using real-world domain-driven design principles.
How to Fix Common Next.js 16 Build Errors (Proxy, Dynamic Rendering & Revalidation)
Learn how to fix common Next.js 16 build errors, understand Proxy, Dynamic Rendering, and Revalidation, and improve your application's SEO and performance.
Why I Built cyber-ui: A Minimal Design System for Developers
How my projects, from backend systems to monitoring tools, led me to create a minimal and personal UI foundation.