Skip to content
Mustafa Erbay
Life · 11 min read · görüntülenme Türkçe oku

Perfect Architecture vs. Working Code: 3 Lessons for the Solo

Examining the dilemma of perfect architecture versus working code, I share pragmatic ways for solo developers to escape over-engineering traps.

100%

There’s an age-old battle in the software world: Perfect Architecture vs. Working Code. If you’re a solo developer working on projects alone, this battle happens within your own mind, before every commit. We talk about design patterns, clean code principles, microservices, and by the end of the day, we’re left with a “perfect” but dead pile of code that hasn’t even had a single line pushed to production. In my 20 years of field experience, I’ve fallen into this trap countless times; each time, I’ve seen that the pragmatic approach, i.e., working code, wins.

For someone developing a product alone, the most valuable resource is time. You need to solve in a single night what large corporate teams take weeks to resolve with architectural meetings. In this post, drawing from my experience developing my own side projects and in the field, I’ll examine 3 fundamental lessons that help solo developers survive and the trade-offs in architectural decisions.

PostgreSQL Index Strategies


1. The Infrastructure Trap: Setting Up Kubernetes When Docker Compose Suffices

One of the most common mistakes solo developers make is trying to set up a massive Kubernetes (K8s) cluster for a project that isn’t even visited by 100 people a day. Perfecting the infrastructure is the most comfortable way to avoid doing the “real work.” You set up K8s, configure ingress, and struggle with certificate renewal automation; but there’s no product that can actually bill customers yet.

I made this mistake myself with one of my side projects initially. Thinking, “What if millions of requests come tomorrow?” I set up a 3-node cluster. The result? I paid an extra $40 bill every month just for the RAM consumption of the control plane components. Moreover, one day when etcd broke, it took me a full 6 hours to get the system back up. Meanwhile, I could have solved the same task with zero overhead using a simple Docker Compose file running on a single VPS.

# docker-compose.yml - The solo developer's lifesaver
version: '3.8'

services:
  web:
    image: python:3.11-slim
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
    volumes:
      - .:/app
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/dbname
    restart: always
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 512M

  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: dbname
    restart: always

volumes:
  postgres_data:

The simple configuration above can easily handle hundreds of requests per second. Furthermore, thanks to cgroup limits, you prevent the web service from causing a memory leak and locking down the entire server. For a solo developer, the perfect infrastructure is the one that takes the least time to maintain and won’t wake you up at night.


2. Over-Normalization in Database Design and ORM Traps

Database normalization rules like 3NF, 4NF, as taught in academic books, look great on paper. However, in practice, an over-normalized database schema for a solo developer means endless JOIN queries and performance bottlenecks. Especially if you’re using an ORM (Object-Relational Mapping) library, it can take time to notice the N+1 query disasters happening in the background.

While working on a production ERP system, we had divided everything into so many tables that showing a simple order status on the operator screen required joining 12 different tables. The SQL queries bloated, and database CPU usage hit 95%. We later solved this problem by denormalizing some data, meaning we kept frequently read fields as JSONB in a single table.

In the SQL example below, you can see how we can solve complex relationships with PostgreSQL’s JSON capabilities in a single query. This approach saves you from writing dozens of lines of ORM code on the client side and avoids burdening the database:

-- Fetching related data as JSON in a single query (N+1 solution)
SELECT 
    orders.id,
    orders.status,
    json_build_object(
        'id', customers.id,
        'name', customers.name,
        'email', customers.email
    ) AS customer,
    (
        SELECT json_agg(json_build_object('item_id', order_items.product_id, 'qty', order_items.quantity))
        FROM order_items 
        WHERE order_items.order_id = orders.id
    ) AS items
FROM orders
LEFT JOIN customers ON orders.customer_id = customers.id
WHERE orders.status = 'pending'
LIMIT 50;

This query retrieves all the data you need hierarchically with a single database round-trip. Your goal as a solo developer is to leverage the power of the database to its fullest, rather than writing hundreds of lines of data transformation logic in your code.


3. Premature Optimization and “You Aren’t Gonna Need It” (YAGNI) Architectures

The “YAGNI” (You Aren’t Gonna Need It) principle is the most important rule a solo developer should hang on their wall. Writing code today for hypothetical future scenarios is the biggest obstacle to finishing a project. For instance, trying to set up message queue systems like RabbitMQ or Kafka for a simple email sending process that runs in the background is a complete waste of time.

In a side project I developed last year, I considered setting up a separate worker service for the welcome email to be sent after a user registers. Then I stopped and asked myself: “How many people are registering per day right now?” The answer: 5. Why should I manage an extra service, a queue mechanism, and a log monitoring layer for this operation? Instead, I used the built-in power of Linux, solving the task with a simple systemd timer or an asynchronous task within the application.

Feature Heavy Queue Architecture (Kafka/RabbitMQ) Simple Systemd Timer / Async Task
Setup Time 4-8 Hours 10 Minutes
Memory Consumption ~512MB - 1GB RAM ~0MB (No extra overhead)
Maintenance Cost High (Version updates, disk full) Negligible
Solo Developer Compatibility Low Very High

If your system truly grows, then changing the architecture will be a welcome problem. But taking on this burden from day one will cause you to get tired before you even start.


4. Microservice Dreams vs. Monolith Reality

Microservices architecture is a solution found by large organizations to allow teams to work independently. For a solo developer to choose a microservices architecture, however, is technical suicide. You cannot single-handedly deal with problems like inter-service communication, distributed tracing, network latency, and eventual consistency.

After witnessing the complexity of managing the deployment and monitoring of 40 different microservices on a bank’s internal platform, I better understood why I always choose the “modular monolith” approach for my own projects. A single codebase (monorepo), a single database, and a single deployment process give you incredible speed.

# /etc/systemd/system/my-monolith-app.service
# Simple systemd definition to keep the solo developer's monolith service live
[Unit]
Description=My Monolith Web Application
After=network.target postgresql.service

[Service]
User=www-data
WorkingDirectory=/var/www/my-app
ExecStart=/var/www/my-app/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 main:app
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

With the systemd service definition above, if your application crashes for any reason (e.g., exceeds memory limits or encounters an unexpected error), it will automatically restart within 5 seconds. You don’t need Kubernetes or complex pod management tools. Just systemd 252+ and clean configuration.


5. Error Handling and Logging: Settling for Journald When Sentry Isn’t Available

Observability is important, but you don’t have to spend your budget and time on it. Instead of paid APM tools or complex ELK (Elasticsearch, Logstash, Kibana) stacks, learning to use the built-in tools provided by the Linux kernel is a great power for a solo developer.

One of the biggest mistakes I’ve seen in system administration is logs growing uncontrollably and filling up the disk. The “Docker disk fire” incident, where the /var/lib/docker/containers directory swells and locks down the entire server, happens to almost every solo developer. To prevent this, you can use journald’s rate limiting and disk size restriction features.

# /etc/systemd/journald.conf.d/size-limit.conf
# Configuration to prevent logs from filling up the disk
[Journal]
SystemMaxUse=2G
SystemMaxFileSize=200M
MaxRetentionSec=1month
RateLimitIntervalSec=30s
RateLimitBurst=10000

With this simple configuration, your logs will never occupy more than 2GB in total, and services attempting to lock down the system by generating more than 10,000 logs per second will be automatically throttled. When there’s an error, you can filter critical errors from the last hour with a single command in the terminal:

# Shows only logs of 'error' level and higher from the last hour
journalctl -u my-monolith-app.service --since "1 hour ago" -p err..emerg --no-pager

Docker Disk Full Solutions


6. Decision Matrix for the Solo Developer: When to Say “Perfect”?

So, at what point should we stop and invest in code quality or architecture? The answer is very simple: When the value produced by the code exceeds its maintenance cost. If the code you’ve written can bill customers tomorrow morning, it’s “working” code, and it doesn’t need to be perfect at that stage.

The decision matrix below can provide you with a practical guide on how to proceed when developing a feature:

Scenario / Need Initial Approach (Working Code) When to Change? (Perfect Architecture)
User Registration / Auth Simple JWT / Session-based single function When user count exceeds 50,000+ or OAuth2/SSO is required
File Uploads Direct save to server disk (/uploads) When disk starts filling up or CDN integration is needed (S3 etc.)
Reporting / Analytics COUNT and GROUP BY on PostgreSQL When reports take longer than 5 seconds to run (Read Replica or ClickHouse)
Payment Integration Direct call via a single API endpoint When daily transaction volume exceeds $10,000+ and Idempotency is required

My firm stance on this is: First run, then validate, then optimize. No matter how perfect the architecture of non-working code is, its value is zero. Your real strength as a solo developer is to use that “agility” and “speed” that large companies sacrifice to bureaucracy to your advantage. Let your code be a little dirty, as long as it’s in production and delivering value to customers.

In the next post, I will explain the PostgreSQL optimization techniques I use in the backend architecture of my own side projects.

Paylaş:

Bu yazı faydalı oldu mu?

Yükleniyor...

How was this post?

Frequently Asked Questions

Common questions readers have about this article.

How can I quickly start my project to get working code instead of perfect architecture?
When starting any new project, I first focus on creating a 'working prototype.' I begin by setting up a repository, establishing a basic framework (e.g., Node.js + Express or FastAPI), and getting a 'hello world' endpoint running. Then, I bring the most critical workflow (like data saving or an API call) to life with just a few lines of code. At this stage, I postpone abstractions like design patterns or dependency injection, and instead, I verify that the code actually works and delivers value to a user. Afterward, as I receive feedback and the requirements become clearer, I gradually improve the architecture; this way, I avoid time waste and the trap of over-engineering.
What are the advantages and disadvantages of setting up Kubernetes instead of Docker Compose? When should I prefer it for a solo developer?
When I tried Kubernetes, I saw powerful features like scalability, automatic restarts, and service discovery; however, for a solo developer handling 100-200 requests daily, these benefits are often overkill. Docker Compose offers a lightweight and fast solution for managing multiple containers on a single machine; its configuration files are simple, and the learning curve is low. Kubernetes, on the other hand, brings additional responsibilities like cluster management, etcd maintenance, and network policies; this means extra time and cost. I prefer Docker Compose unless my project's traffic and complexity exceed a certain threshold (e.g., 5,000 concurrent requests); I only switch to Kubernetes for large-scale SaaS or situations requiring shared infrastructure within a team.
What were the biggest mistakes I encountered while setting up Kubernetes, and how did I solve them?
In my first attempt, my biggest mistake was overlooking the RAM consumption of control plane components (etcd, kube-apiserver); when I set up a 3-node cluster, I saw an unexpected $40 increase in my bills. Also, because I had inadequately configured pod health checks, I frequently encountered 'CrashLoopBackOff' errors. To solve these issues, I first strictly defined the resource limits for the nodes and added an etcd backup policy. By defining health checks, I enabled automatic pod restarts and used standardized configurations with Helm charts. As a result, by integrating monitoring and log aggregation tools (Prometheus + Grafana), I was able to detect problems early and significantly reduce intervention time.
Is a microservices architecture a better choice for every project? Explain whether the general consensus on this is correct.
In my 20 years of experience, I've seen that microservices architecture truly adds value in large-scale systems requiring high traffic, independent teams, and frequent release updates. However, for a solo developer, this architecture introduces extra complexities like inter-service communication, data consistency, and distributed tracing. In most cases, a monolithic structure provides faster development, fewer bugs, and lower operational costs. Therefore, unless the project's complexity and scale necessitate microservices, implementing a clean architecture within a single codebase is a more pragmatic approach. The general consensus's misconception that 'everything should be microservices' is, in my experience, only valid in specific scenarios.
ME

Mustafa Erbay

Sistem Mimarisi · Network Uzmanı · Altyapı, Güvenlik ve Yazılım

2006'dan bu yana sistem mimarisi, network, sunucu altyapıları, büyük yapıların kurulumu, yazılım ve sistem güvenliği ekseninde çalışıyorum. Bu blogda sahada karşılığı olan teknik deneyimlerimi paylaşıyorum.

Kişisel Notlar

Bu notlar sadece sizde saklanır. Tarayıcınızda yerel olarak tutulur.

Hazır 0 karakter

Comments

Server-side AI Moderation

Comments are AI-moderated server-side and stored permanently.

?
0/2000

Server-side AI moderation

✉️ Free · No spam · Unsubscribe anytime

Get notified about new posts

New content and technical notes — straight to your inbox.

  • 📌
    Best of the week Single most-worth-reading post
  • 🔧
    Toolbox notes Real tools I used this week
  • 🧠
    Behind-the-scenes Notes that don't make it to blog

We don't spam. Unsubscribe anytime. · Tracked only by Umami (self-hosted, no Google).

Your Reading Stats

0

Posts Read

0m

Reading Time

0

Day Streak

-

Favorite Category

Related Posts