ArchitecturePublished on 6 min read (1370 words)Author: Ekya Muhammad

The Architecture I Actually Need

Why architecture should be justified by the problem, the team, and the constraints—not by how many technologies appear in the diagram.

Tags: #FastAPI #WebSocket #SSE #Qdrant #LLM #Infrastructure

I like architecture diagrams.

There is something satisfying about seeing a system divided into clear components: an API layer, a worker, PostgreSQL, a vector database, a local LLM, a reverse proxy, real-time communication, and a separate client. Each box has a responsibility. Each arrow suggests deliberate design.

I have explored architectures involving FastAPI, REST, WebSocket, SSE, background workers, PostgreSQL, Qdrant, LLM APIs, local models through Ollama, Flutter, Nginx, Tailscale, and VPS infrastructure. That exploration has been useful because it taught me what these components can do.

It also taught me a less exciting lesson: knowing that I can add a component does not mean the system needs it.

The architecture I want to build today is increasingly the architecture I can justify.

Complexity is attractive because it looks like capability

Modern backend development makes sophisticated building blocks accessible. I can add a queue, put a vector database next to PostgreSQL, stream tokens over SSE, maintain bidirectional state over WebSocket, run an LLM locally, and place the services behind Nginx.

All of those technologies solve real problems.

The temptation is to treat their presence as evidence that the architecture is advanced.

But architecture is not a technology collection. A system with more components has more capabilities, but it also has more failure modes. There are more processes to start, more connections to observe, more configuration to synchronize, more logs to inspect, and more deployment steps that can go wrong.

If the problem does not need those capabilities, the complexity is not neutral. It becomes work.

REST is often enough until the interaction says otherwise

REST is easy to underestimate because it is familiar.

For many operations, a normal request and response are exactly what the system needs. Create a record. Fetch a list. Update state. Trigger an operation and return a result.

WebSocket becomes valuable when the communication pattern is genuinely bidirectional and long-lived. A client may need frequent server updates while also sending state changes without repeatedly creating new request cycles.

SSE fits a different shape. When the server mainly needs to stream updates in one direction—such as incremental progress or generated text—SSE can be simpler than maintaining a fully bidirectional WebSocket connection.

The question I now try to ask is not, "Which real-time technology is better?" It is, "What communication behavior does the user experience require?"

If polling every few seconds is acceptable, even that may be the correct answer. Architecture starts with the interaction contract, not the trendiest transport.

Background workers should protect boundaries that matter

The same reasoning applies to workers.

A background worker is useful when a task is too slow, unreliable, or independent to keep inside the lifetime of a request. AI inference, long-running processing, document transformation, or other expensive jobs can be reasonable candidates.

Moving work to a worker also introduces a queueing model. Now I have to think about retries, idempotency, job state, failure visibility, and what the client sees while the work is incomplete.

That is a good trade when asynchronous execution solves a real product or reliability problem. It is unnecessary overhead when the task is fast and predictable.

A worker should be a consequence of the workload, not a default box in the diagram.

A vector database is not a requirement for using an LLM

AI projects create another form of architectural gravity. Once retrieval-augmented generation enters the discussion, adding a vector database can feel automatic.

Qdrant and other vector databases are useful when the system needs semantic retrieval across embedded content at a scale or query pattern where specialized vector search is justified.

But not every AI feature needs retrieval. Not every retrieval problem needs a separate vector database. Small, structured datasets may be searchable in simpler ways. Some application features need deterministic database queries rather than semantic similarity. Some prompts already contain all the context the model requires.

Using a vector database without a retrieval problem creates storage, indexing, synchronization, and operational work without a clear benefit.

The right question is not, "Where can I use embeddings?" It is, "What information does the model need, and what is the simplest reliable way to retrieve it?"

Local LLMs made constraints impossible to ignore

Running models locally through tools such as Ollama made architectural trade-offs more physical for me.

A cloud API hides most of the hardware. I think about request cost, latency, quotas, data handling, and provider dependency, but I do not have to decide whether the model fits in available VRAM or system memory.

Local inference removes some of those external dependencies and can give me more control, but the hardware becomes part of the design. Model size affects memory. Quantization affects resource use and output quality. CPU and GPU capability affect latency. Concurrent requests can become a capacity problem quickly.

Because my hardware is limited, I cannot treat those constraints as theoretical.

That has been useful. A resource limit forces me to decide what the system actually needs. Maybe a smaller model is sufficient. Maybe an API is more practical for a specific workload. Maybe a local model should handle only certain tasks. Maybe the product should avoid an expensive generation step entirely.

Constraint turns architecture from preference into reasoning.

Infrastructure has a maintenance price

Nginx, Tailscale, and a VPS can make a system flexible and accessible, but they also expand the operational surface.

A reverse proxy adds routing and TLS concerns. Private networking changes how services discover and reach each other. A VPS needs deployment, process management, updates, monitoring, and security decisions.

None of this is an argument against infrastructure. It is an argument for counting operations as part of architecture.

A design is not finished when the request path works on a diagram. Someone has to keep the path working.

For a small team—or for one student developer—that "someone" may be the same person writing the application code. Every additional service therefore competes with feature work and debugging time.

Distributed systems create coordination problems before they create scale

Breaking a system into services can isolate responsibilities and scale components independently. It can also create network boundaries where there used to be function calls.

Now data consistency, retries, service availability, timeouts, and observability matter in new ways. A local failure can become a partial system failure. Debugging may require correlating events across processes.

For a small system, that cost can arrive long before the scale benefit does.

This is why I have become more comfortable starting with a monolithic application or a small number of processes when the problem allows it. I can still preserve internal boundaries in the code. I can extract a service later if the workload or organizational structure justifies it.

Starting simple does not mean refusing to scale. It means delaying irreversible complexity until there is evidence for it.

The architecture should follow the constraint

The most useful architecture discussions I have now are constraint discussions.

How many users are expected? What latency is acceptable? What data must remain relational? Which tasks can fail independently? What hardware is available? Who will operate the system? How much time does the team have? What happens when the network is unavailable? How expensive is an external API compared with maintaining a local model?

Those questions reduce the space of reasonable designs.

Sometimes the answer will still be WebSocket, a worker, Qdrant, a local LLM, and several deployment components. When those pieces are justified, I want to understand them well enough to operate them.

But I no longer want them simply because they make the architecture look complete.

Start simple, then earn complexity

The principle I keep returning to is simple: start with the smallest architecture that satisfies the current constraints, measure where it fails, and add complexity when the evidence justifies the cost.

That principle is not anti-technology. It actually makes learning technology more useful. I can study WebSocket without forcing it into every project. I can understand vector databases without assuming every LLM application requires one. I can experiment with local inference while still choosing an API when the operational trade-off is better.

The architecture I actually need is not the one with the most boxes.

It is the one whose boxes I can explain.