Engineering “Interfaces That Feel Alive” – My Design Philosophy

Portfolio:

The Core Concept

Websites shouldn’t just be viewed; they should be felt. When we talk about “Awwwards-level” digital experiences, it isn’t just about a flashy color palette or a trendy typeface. It’s about that organic flow-the thin line between a static UI and an immersive environment.

The Vision: Motion-First Identity

My portfolio (screenshot above) is built on a single principle: Interfaces that feel alive – not just animated. In this hero section, I focused on three pillars to define my digital identity:

The Living Core: The central organic blob isn’t a pre-rendered video or a simple loop. It’s a performance-driven shader designed to give the interface a “pulse” that responds to the environment.

Typography & Hierarchy: By using high-contrast, minimalist Sans-Serif, I let the message breathe while the motion does the heavy lifting.

Technical Sophistication: Every interaction is locked at a cinematic 60FPS to ensure the premium feel remains buttery smooth.

The Creative Tech Stack

To bridge the gap between functional UI and high-end aesthetics, I utilized a stack that balances performance with creative freedom:

  • Next.js: For a robust, SEO-friendly architecture.
  • React Three Fiber (R3F): Bringing 3D into the DOM with seamless state management.
  • Custom GLSL Shaders: Crafting unique visual textures that go beyond standard CSS.
  • GSAP & Framer Motion: For orchestrating complex layout transitions and micro-interactions.

Beyond the Code

For me, code is more than just logic-it’s a medium for Digital Architecture. As a Creative Frontend Developer and WebGL Specialist, I am constantly pushing the boundaries of how we interact with the web.

The goal is always the same: Move away from “templates” and toward “experiences.”

I’d love to hear from other creative engineers:
How do you balance heavy WebGL graphics with web performance? Let’s discuss in the comments!

The Agent Reproducibility Paradox: Debugging Non-Determinism in Production

The Agent Reproducibility Paradox: Debugging Non-Determinism in Production

Your production agent made a decision that broke something. A user reports a weird output. A compliance audit flags unexpected behavior. You investigate.

You grab yesterday’s logs. Same prompt, same tools, same input. You replay it locally. Different output.

Welcome to the agent reproducibility paradox.

The Root Problem: Agents Aren’t Deterministic

Traditional software bugs are deterministic. Same input + same code = same crash, every time. This is why debuggers work: you can reproduce the exact state, step through execution, find the bug.

Agents don’t work this way.

Consider a single agent API call:

Input prompt: "Process this transaction"
Model: Claude 3.5 Sonnet
Temperature: 0.2
Available tools: [payment_api, audit_log, fraud_check]

Yesterday, this returned output_a. Today, same prompt, same config:

  • The model version updated (Anthropic released a patch)
  • The temperature seed drifted (framework version changed)
  • The context window shrank (you added more system prompt)
  • The tool response latency changed (payment API slowdown)
  • Token encoding shifted (model training data evolved)

Now you get output_b.

Both are “correct” given the current system state. But they’re different. And you can’t reproduce output_a anymore because the system that produced it no longer exists.

Why This Breaks Debugging

Deterministic debugging works because:

  1. You identify the problem
  2. You reproduce it locally
  3. You step through execution with a debugger
  4. You find where state diverges from expected
  5. You fix it

Non-deterministic systems break this cycle:

  1. You identify the problem (user reports output_b broke something)
  2. You try to reproduce it (can’t—model updated)
  3. You look at logs (they show parameters, not execution details)
  4. You check tool responses (they’re cached summaries, not raw data)
  5. You give up (ship a hotfix, hope it works)

This isn’t speculation. This happens every week in production AI teams.

Engineer A: “Agent returned false for is_fraud_check_passed(). Spent 2h debugging. Turned out the fraud_check API had a one-minute outage. By the time I investigated, the API recovered. The agent was right, the system state was unstable, and logs didn’t capture it.”

Engineer B: “Same prompt, exact same tools, returns different classifications today vs yesterday. No code changes. Model had a micro-update. Context window is now 3 tokens shorter. Spending days trying to figure out if the agent broke or if the system did.”

Engineer C: “Compliance audit asks: prove your agent followed rule X yesterday. I have logs that show the agent saw the rule and processed the transaction. But the rule was ambiguous. The model interpreted it one way. The log can’t prove the interpretation was correct or incorrect. Just that it happened.”

Why Non-Determinism Matters More for Agents

In monolithic systems, non-determinism is rare and usually a bug. In agent systems, it’s fundamental.

Your agent:

  • Calls 5+ different APIs (each has variable latency, variable response format)
  • Processes variable-length context (different conversation histories)
  • Uses temperature/sampling (inherent randomness in language generation)
  • Delegates to multiple sub-agents (each adds more entropy)
  • Relies on external state (databases, services, market data)

Each of these adds non-determinism. Together, they mean:

The probability of reproducing exact execution is approximately zero.

For compliance, this is catastrophic. EU AI Act Article 6 requires AI systems to maintain audit trails. But you can’t maintain an audit trail for execution that you can’t reproduce.

Regulators ask: “Prove your agent didn’t discriminate against this applicant.”

You show logs. They ask: “But can you reproduce the exact decision today?”

You can’t. Model updated. You show logs again. They say: “Logs are claims made by your system. We need proof witnessed by someone without stake in the outcome.”

You’re stuck.

What Reproducibility Actually Requires

Real reproducibility isn’t “same input → same output.” It’s “prove this exact execution happened as logged, at a specific time, without tampering, witnessed by a party that can’t deny it.”

Three requirements:

  1. Cryptographic capture of execution — Every step (model call, tool invocation, response, decision) is timestamped, hashed, and signed at execution time. Not logged afterward (logs can be edited). Captured live.

  2. Independent witness — A third party observes and attests to the execution. Not you (you have motive to edit). Not the model provider (they have motive to hide issues). A party with legal liability for false claims.

  3. Proof you can’t forge — Signatures, hashes, timestamping using infrastructure you don’t control. If you try to alter the execution record, the tampering is cryptographically detectable.

Example: Your agent calls payment_processor.charge() with amount €100.

Without reproducibility proof, you log:

agent_id=123, action=charge, amount=100, response=success

With reproducibility proof, an independent witness timestamps and signs:

Time: 2026-03-20T14:32:17.451Z
Call: POST /payment_processor.charge(amount=100)
Request hash: a7f2e9d... (SHA256)
Response: success
Response hash: f1b4c2e... (SHA256)
Witnessed by: trust_layer_v1.2.5
Signature: MIIDe9w... (RSA-2048, key_id=tl_prod_2026)

Now you can prove the call happened exactly as logged, at a specific time, witnessed by infrastructure that can’t deny it.

Tomorrow, if regulators ask: “Did your agent charge €100 at 14:32:17 UTC?” You show the witness signature. They verify the signature against the public key. The signature is cryptographically valid. The agent charged exactly what it claimed.

You can’t produce a fake signature (you don’t have the private key). The witness can’t deny it (the signature is their legal attestation).

That’s reproducibility that holds up in court.

Why Logs Aren’t Enough

You might think: “Can’t I just log more detail?”

No. Here’s why:

Logs are claims made by the system that performed the action. Your agent logs what it did. Your payment processor logs what it processed. Your database logs what it committed. But logs are created by the parties with motive to hide problems.

If your agent made a mistake, it won’t log the mistake—it logs success.
If your payment processor had a bug, it won’t log the bug—it logs the transaction.
If your compliance team wants to hide a violation, they won’t log the violation.

Logs are useful for internal debugging. They’re insufficient for compliance, audit, or legal disputes, because they’re self-serving.

Proof requires a third-party witness that observed the system’s behavior and certified it. The witness can’t lie (cryptographic proof) and can’t deny it (legal liability).

The Multi-Agent Amplification

Single-agent systems have one layer of non-determinism. Multi-agent systems multiply it.

Agent A queries Agent B. Agent B queries your API. Agent C synthesizes their outputs. An orchestrator delegates to Agent D. Each step:

  • Agent A’s output depends on model state, temperature, context length
  • Agent B’s response depends on its own non-determinism plus what it received from A (which might be corrupted)
  • Your API’s response depends on database state, cache state, load
  • Agent C’s synthesis depends on how it interpreted A and B’s outputs
  • Orchestrator’s delegation depends on all of the above

With n agents, you have n sources of non-determinism plus n opportunities for misinterpretation.

Proving execution required proving all n agents’ behavior. Logs won’t do it. Self-attestation won’t do it. You need n independent witnesses.

The Solution: Reproducibility as System Boundary Function

Reproducibility isn’t something your agent can solve alone. Your logging framework can’t solve it. Your orchestrator can’t solve it.

It requires an independent verification layer that:

  • Observes all agent execution (API calls, model invocations, responses)
  • Timestamps and signs everything at execution time
  • Can’t be tampered with by agent code or application logic
  • Provides cryptographic proof verifiable by external auditors

This layer sits outside your agent, outside your infrastructure, outside any single provider’s control.

When your agent runs, the verification layer watches:

  1. Agent sends request to Claude API
  2. Witness timestamps, hashes request
  3. Claude returns response
  4. Witness hashes, signs, timestamps response
  5. Witness stores proof in immutable ledger
  6. Agent continues execution

Later, auditors can verify:

  • The exact request and response (cryptographic hash)
  • The exact timestamp (3rd-party authority)
  • The signature validity (witness’s public key)
  • The ledger immutability (cryptographic chain)

Your agent doesn’t need to change. The verification is transparent.

Implications for Your Team

  1. You can now reproduce execution — Not by running the same code (model may have updated), but by showing the witness signature. Auditors can verify the proof without trusting you.

  2. Multi-agent complexity becomes auditable — Each agent’s behavior is independently witnessed. No single point of trust.

  3. Compliance moves from “our logs show we’re compliant” to “here’s our witness proof of compliance.” These are different legal positions.

  4. You can debug production incidents with real data — Instead of “logs show parameter X was Y,” you have “witness proof shows parameter X was Y at timestamp T with response Z from service W.” No ambiguity.

The Takeaway

Your agents can’t be deterministic. But their execution can be verifiable.

Logs won’t satisfy regulators. Proof will.

Start thinking about reproducibility not as “same input = same output,” but as “I can prove this execution happened exactly as claimed, witnessed by infrastructure I don’t control, using cryptography I can’t forge.”

That’s what enables AI agents to scale into regulated environments.

why start i use redux toolkit? why not classic redux?

I Was Writing 4 Redux Functions for Everything… Until Redux Toolkit Changed It

When I first started learning Redux, I thought this was just how things worked.

If I needed to manage a simple feature, I had to write:

  • Action types
  • Action creators
  • Reducers
  • Store configuration

At first, it felt “professional”.

But after a few days, it started to feel… repetitive.

And honestly, a bit exhausting.

The Problem With Classic Redux (From My Experience)

Let me be real.

Redux is powerful. No doubt.

But as a beginner, I kept asking myself:

Why do I need so much code just to update a simple state?

For one feature, I was jumping between multiple files:

  • actions.js
  • reducers.js
  • store.js

And inside those files:

  • writing constants
  • writing functions
  • writing switch cases

It felt like I was doing the same thing again and again.

Then I Discovered Redux Toolkit

Today, I finally spent time understanding Redux Toolkit.

And this is where everything started to change.

Redux Toolkit is basically the modern way of writing Redux.

It removes the unnecessary complexity and keeps the power.

The Moment That Changed Everything

There were two functions that completely changed my understanding:

1️⃣ configureStore

Before:

I had to manually use:

  • createStore
  • combineReducers
  • applyMiddleware

Now:

Just one function:

const store = configureStore({
  reducer: rootReducer,
});

That’s it.

Everything is handled internally.

No extra setup.

2️⃣ createSlice

This one honestly surprised me.

Before, I had to write:

  • action types
  • action creators
  • reducers

Separately.

Now:

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment(state) {
      state.value++;
    },
  },
});

And just like that:

  • actions are created automatically
  • reducer is created automatically

One function replaces 3–4 things.

The Best Part: Cleaner Code

What I really liked is how clean everything feels now.

  • No switch statements
  • No scattered files
  • No repeated boilerplate

Everything lives in one place.

And for the first time, Redux felt… simple.

A Small But Important Realization

Another thing I learned today:

You don’t have to switch everything at once.

You can use:

  • classic Redux
  • Redux Toolkit

in the same project

And both connect to React in the same way.

That means you can migrate step by step.

No pressure.

What I Learned Today

Today wasn’t about building something big.

It was about understanding:

How to write better Redux code.

How to reduce unnecessary complexity.

How to focus more on logic instead of boilerplate.

Final Thoughts

Before today, Redux felt like a lot of work.

After understanding Redux Toolkit, it finally started to feel practical.

If you’re learning Redux right now, my honest advice:

Don’t just learn classic Redux.

Learn Redux Toolkit early.

It will save you time, energy, and a lot of confusion.

Cómo la IA Finalmente Me Ayudó a Lanzar el Blog que Venía Postergando desde 2024

cover

cover

Todo ingeniero tiene un cementerio de side projects. En el mío había un blog. Dos veces.

La primera vez que pensé en crear un blog fue a principios de 2024. Tenía cosas que decir — cosas reales, de proyectos reales. Decisiones de arquitectura que salieron mal. Patrones serverless que se veían perfectos en la pizarra y se desmoronaban en producción. Las historias de guerra que uno acumula después de años construyendo sistemas de los que otros dependen.

Pero entonces: arrancó un nuevo sprint. Un cliente necesitaba algo urgente. Una migración de infraestructura no podía esperar. Y el borrador del blog se quedó en una nota de Notion, acumulando polvo digital, junto a otros tres proyectos de “ya lo retomo cuando pueda”.

Esto no pasó una vez. Pasó dos. Dos intentos. Dos borradores abandonados. El mismo patrón cada vez: dos pasitos para adelante, luego la vida real aparecía y cerraba silenciosamente la pestaña.

El Problema Real No Era el Tiempo. Era la Energía de Arranque.

Lo que eventualmente entendí fue esto: no es que no tuviera tiempo. Era que el costo de sentarme a producir algo listo para publicar era suficientemente alto como para que siempre perdiera contra cualquier otra cosa que compitiera por mi atención.

Escribir un post implicaba: redactar, editar, encontrar o crear una imagen, darle formato para la web, pensar en SEO, publicarlo, y luego repetir todo eso para cada plataforma donde quería que viviera. Cada uno de esos pasos es pequeño por separado. Juntos, forman una pared.

Y si eres alguien que diseña sistemas profesionalmente, sabes exactamente qué pasa cuando la fricción en un proceso es demasiado alta: la gente lo esquiva o lo abandona.

Yo estaba esquivando mi propio blog.

Entonces Decidí Tratarlo Como un Problema de Ingeniería

En 2025 finalmente construí el blog — pero lo encaré de forma diferente. En lugar de buscar una fuerza de voluntad que no tenía, reduje la energía de arranque.

El blog en sí fue construido principalmente con vibe coding usando Claude Code. No voy a pretender que escribí cada línea desde cero. Tenía una visión clara de lo que quería: un sitio de portafolio rápido y limpio, con sección de blog, soporte de internacionalización, Sanity CMS en el backend, y algo que se sintiera como yo y no como un template genérico. Claude Code me ayudó a pasar de idea a implementación funcionando mucho más rápido de lo que hubiera podido solo. Yo guié el proceso, revisé el código, tomé las decisiones — pero no me quedé bloqueado en los detalles de implementación.

¿Las imágenes? Generadas con Gemini (Nano Banana Pro). Escribo un prompt descriptivo para cada post, enfocado en un estilo de ilustración tech limpio, y obtengo un visual que le hace justicia al artículo sin tener que pasar horas en Figma ni buscar stock photos que “más o menos” sirvan.

El contenido — incluyendo este post — está escrito con asistencia de IA. No generado de forma automática y publicado a ciegas, sino redactado, refinado y moldeado con la IA como compañero de pensamiento. Las ideas son mías. Las experiencias son mías. La IA me ayuda a sacarlas de la cabeza y llevarlas a la página más rápido.

De Qué Va a Tratar Este Blog

Más allá de la historia de cómo fue construido, quiero ser claro sobre lo que estoy haciendo acá.

Este blog va a cubrir cosas que realmente viví durante mi carrera como Cloud Architect y Software Engineer. Eso incluye:

  • Decisiones de arquitectura y los trade-offs reales que hay detrás
  • Sistemas serverless y event-driven — cuándo funcionan y cuándo no
  • Integrar sistemas legacy sin romper producción
  • Diseño de SaaS multi-tenant en AWS
  • Liderazgo técnico y cómo explicar arquitectura a personas que no son arquitectos
  • Y sí, alguna que otra anécdota del campo — porque ciertas lecciones solo quedan cuando vienen con una historia

El objetivo no es ser un sitio de tutoriales. Hay miles de esos. El objetivo es compartir el razonamiento detrás de las decisiones — el por qué, no solo el cómo.

Un Detalle de Ingeniería Más: El Pipeline de Publicación Cruzada

Porque al parecer soy incapaz de resolver un problema sin construir también un sistema alrededor de él:

Armé un pipeline para que cuando publique un post aquí, se replique automáticamente a Hashnode y dev.to. Escribir una vez, publicar en todas partes.

Si eres desarrollador, ya sabes por qué esto importa. La mejor estrategia de distribución de contenido es la que realmente vas a mantener. Copiar y reformatear posts manualmente en tres plataformas distintas es exactamente el tipo de fricción que mató mis intentos anteriores. Así que lo eliminé.

El sistema maneja las diferencias de formato entre plataformas, propaga el contenido, y me deja enfocarme en escribir en lugar de en la logística de publicación.

El Punto Principal

No estoy escribiendo este post para decirte que la IA es mágica. No lo es. Es una herramienta — y como cualquier herramienta, su valor depende completamente de cómo la uses.

Lo que la IA hizo por mí fue bajar el costo de empezar. No reemplazó el criterio, la experiencia ni las ideas. Eliminó la barrera de energía de arranque que había mantenido un blog a medio terminar en mi cementerio de proyectos por más de un año.

Para ingenieros en particular, creo que esta es la aplicación más subestimada de estas herramientas ahora mismo. No reemplazar tu trabajo central — sino eliminar la fricción circundante que te impide lanzar las cosas que realmente te importan.

El blog existe ahora. Lo estás leyendo.

Eso ya es suficiente prueba de concepto para mí.

Writing code for the sake of code

It has come for me to write something on a topic every developer has heard at some point. With AI being present into our daily lives and AI assisting developers, I feel this would become more of an issue. It’s a problem that will naturally occur in the world of programming. Writing code just to write code. What does that mean? In my opinion, it means coding without any thought. Writing code without a clear purpose or plan can lead to various issues, especially for software developers working in a collaborative or long-term project setting. Even with vibe coded application these issues can compounded if not addressed.

Code Quality and Maintainability

Random code is often unstructured and lacks comments or coherent design, making it hard to read, maintain, or extend in the future. Clean, organized code is essential for a project to evolve without accumulating “technical debt” that could slow development and increase costs. Now I know that the concept of “clean code” itself is a polarizing discussion among software engineers and it has been that way for a long time. The clean code debate continues to exist in the programming world.

Projects in the corporate world go by fast, there is no time to refactor or “improve” the codebase, unlike personal projects which are created with no time restrictions. Even if the code makes me cringe I can’t do anything to it unless I get approval from a tech lead or another dev on the team. So “bad code” continues to exist because “it works” mentality. This is very subjective and why code quality is the most debatable topic.

Increased Bug Risk

Writing code without a plan usually means skipping the design and testing phases, which increases the likelihood of introducing bugs. This can result in more time spent debugging and fixing issues down the line. I don’t hate testing and see it as a necessary evil. Reasons others dislike testing due to its repetitive nature and or denial resulting in “Hey I did it myself, how can there be any errors? The tests are wrong.” attitude. Worst case, the testing phases are skipped due to a lack of care or running out of project time. Mistakes can slip through the cracks and can be fixed. I don’t want it worse by keeping moving without assessing the issue. Having a clear design before starting addresses obvious issues.

Wasted Resources

Coding without direction can lead to wasted time and effort if the code turns out to be unnecessary or incompatible with the rest of the project. In a team setting, this also wastes resources, as other developers may need to review or rework the code. Time is the biggest hurdle for a project. A project with tight deadlines is something you don’t want to waste time on. Wasted tokens can also add up due to awful prompts.

Poor Scalability

Random code often doesn’t consider scalability, which can be problematic as projects grow. Good design anticipates future needs and changes, whereas unplanned code usually requires significant refactoring to meet new requirements. The worst case is that starting from scratch over again. Scalability is another hurdle when migrating an existing project or updating it.

Any stories of when vibe coding went wrong? Share in the comments!

Engineering a LangGraph UI Pipeline

Build an Agentic Pipeline for Frontend Development: Design Decisions, Trade-offs, and Practical Lessons

1. The Token Economy
Managing tokens is a real challenge, i am using Groq’s free tier (250k TPD)[
openai/gpt-oss-120b].

  • Audit Every Step
    Do not wait until the end of the project to optimize. Monitor token consumption at every node. audit at every step (i used langsmith for tracing the node calls)

  • The best call is the one you never make
    A pipeline must treats the LLM as a last resort for reasoning. Every time you replace a prompt with a regex or a hardcoded template, you’re shrinking your ‘LLM Surface Area’ — which is the only real way to kill latency and save your budget

LLMs are elite at reasoning and planning. They can architect a component structure or plot a multi-step migration. But (2+3) can be done better by cpu then transformer : llm.invoke(“2 + 3”).

2. The “Eraser-First” Workflow
Don’t start with code. I found success by extracting raw logic from GPT-4/Claude and then manually refining it on Eraser.io.

  • Before touching your IDE, define every node by:
    Strict Data Contracts: Define the Input/Output schemas clearly.

  • The Pivot:
    Your initial diagram is a hypothesis. As you observe real-world model responses, be prepared to refactor nodes. Rigid architectures fail; adaptive graphs win.

3. Radical Latency Reduction: Parallelism
In a linear graph, your latency is the sum of every node’s response time. In a production-grade graph, your latency should only be the length of the longest path.

  • Fan-Out/Fan-In Architecture:
    I restructured my graph so that independent tasks — like generating components while simultaneously drafting pages code files— run in parallel.

  • The Multi-Thread Advantage:
    Moving from sequential flows to parallel node execution reduced my total execution time by a major factor. If nodes don’t depend on each other’s data, they shouldn’t wait for each other to finish.

4. Avoiding the Tool-Calling Trap
Giving an LLM a massive toolbox feels powerful, but it increase latency . Every tool added introduces a “reasoning cycle” where the model must decide how to frame the call.

  • Minimize the Toolset:
    Only provide tools for tasks the LLM cannot predict or calculate via code.

  • Deterministic Prediction:
    LLMs often call tools in loops to “figure out” a solution. If you can manage that logic via pre-defined code paths or “prediction” nodes, do it. Don’t let the LLM waste time deciding what you, as the developer, already know.

5. Debugging

  • Isolated Debugging:
    I Uses LangSmith from Node #1. Validating each node’s output in isolation prevents “spaghetti logic.” If you wait until the full graph is finished to debug, you will never reach a stable state.

Agentic development is a system optimization problem. By prioritizing parallel node execution (low latency), replacing redundant LLM calls with deterministic logic(saving tokens and latency), and enforcing strict schema contracts(better structured outputs), you move from “experimental wrappers” to production-grade software.

Why Your PostgreSQL Keeps Running Out of Connections

PostgreSQL connection errors are one of those things that look terrifying when they hit production.

I used to think:
“Why is the database refusing connections?”
“Did something crash?”
“Is the server overloaded?” 😅

Recently, while working on a production system, I ran into the classic TooManyConnectionsError. Not once twice. On two different services. Same database, same root cause.

That experience helped me clearly understand why this happens and how to fix it properly.

This post is me breaking that down in a simple way, based on what actually worked.

What does TooManyConnectionsError actually mean?

In simple terms, PostgreSQL has a limit on how many connections it allows at the same time.

This limit is set by max_connections and is usually:

  • 50–100 for small/basic tiers
  • 100–200 for general purpose tiers

When your application tries to open more connections than this limit, PostgreSQL says no. That’s the error.

The important thing to understand:

The database isn’t down. It just has no room for new connections.

But I’m using a connection pool… why is this still happening?

This is the part that confused me.

I was already using a connection pool. Every tutorial says “use a pool” and I did. So why was I still exhausting connections?

Here’s what I found out:

The problem wasn’t that I didn’t have a pool. The problem was I had too many pools.

Let me explain.

How the bug actually works

Most people write their database client class something like this:

class DatabaseClient:
    def __init__(self):
        self.pool = create_pool(min=2, max=10)

Looks clean right? Every instance gets its own pool. Professional.

But here’s what happens in a real application:

  • Health checker creates a DatabaseClient() → pool of 10
  • Dependency checker creates a DatabaseClient() → another pool of 10
  • Each worker/task creates a DatabaseClient() → another pool of 10 each

If you have 6 workers, that’s already:

10 + 10 + (6 × 10) = 80 connections from ONE container

Run 2 containers? That’s 160.

Your database allows 100.

💥 TooManyConnectionsError

And the worst part? Each pool individually looks reasonable. It’s only when you add them all up that it explodes.

The fix is embarrassingly simple

One process, one pool. Everything borrows from it.

Instead of each class creating its own pool, create ONE pool at the module/process level and every instance uses that shared pool.

# Created once at module level
_shared_pool = None

def get_shared_pool():
    if _shared_pool is None:
        _shared_pool = create_pool(min=2, max=5)
    return _shared_pool

class DatabaseClient:
    def get_connection(self):
        return get_shared_pool().acquire()

Now it doesn’t matter how many DatabaseClient() instances you create. They all share the same 5 connections.

That’s it. That’s the fix.

Things I learned the hard way

Here are some gotchas that bit me:

1. The close() trap

If your client class has a close() method that closes the pool, and some other code calls it mid-process congratulations, you just killed the pool for everyone.

Make close() a no-op on individual instances. Only close the shared pool when the entire process shuts down.

2. The cascade effect

When the database runs out of connections, it doesn’t just fail your query. It also fails your health check. And when the health check fails, your orchestrator thinks the container is unhealthy and might restart it. Which creates new pools. Which makes things worse.

I literally got two alerts 30 seconds apart. First one: TooManyConnectionsError. Second one: dependency_check_failed. Same container, same root cause. One bug, two pages.

3. Make pool size configurable

Use an environment variable like PG_POOL_MAX_SIZE=5. When you’re debugging at 2 AM, you don’t want to redeploy just to change a number.

4. Do the napkin math

Before deploying, always calculate:

pool_max_size × max_replicas < max_connections - admin_headroom

Example:

  • Pool max: 5
  • Total replicas across all services: 10
  • Total: 50
  • Database max_connections: 100
  • Admin headroom: 20
  • 50 < 80 ✅

If the math doesn’t work, either reduce pool sizes or put a connection pooler like PgBouncer in front of the database.

Common mistakes I see (and made myself)

  • Creating a new pool per class instance instead of sharing one
  • Not accounting for multiple services hitting the same database
  • Forgetting that container scaling = more pools = more connections
  • Not closing pools gracefully on shutdown (idle connections linger)
  • No rollback plan when the fix itself has a bug 😅

A simple checklist before you deploy

Before pushing connection pool changes to production:

  • ✅ Single shared pool at module/process level
  • ✅ Pool size is configurable via env variable
  • ✅ close() on individual instances is a no-op
  • ✅ Shared pool closes on process shutdown
  • ✅ Napkin math checks out
  • ✅ Code compiles/passes syntax check (trust me on this one 😅)

Final thoughts

Connection pool exhaustion doesn’t have to be scary.

If I had to summarize everything in one line:

Many pools = trouble. One shared pool = peace.

The bug is almost never that you forgot to use a pool. It’s that you accidentally created too many of them.

Once you understand this, TooManyConnectionsError stops being a 1 AM panic and becomes just another thing you know how to handle.

Hope this helps someone who just got paged for the first time because their database “ran out of connections”… 🙂

The Systems Behind AI: Understanding LLMs and Their Place in Engineering

You’ve used AI.
You’ve explored it.
You’ve probably even built with it.

But behind many of the AI systems we interact with today is something more fundamental.

The Large Language Model (LLM).

LLMs power a large portion of modern AI systems and AI agents. They act as the core intelligence layer behind many tools and applications.

So before building more systems with AI, an important question becomes:

What exactly are LLMs?

What Are LLMs
A Large Language Model (LLM) is a computational model trained on vast amounts of data and designed for natural language processing, especially language understanding and language generation.

These models learn patterns in language and use those patterns to generate responses, reason through prompts, and assist in solving problems.

At a high level, LLM systems typically involve:

Encoders: which process and understand input information
Decoders : which generate responses or outputs
Modern LLMs combine these mechanisms to process context, relationships between words, and the structure of language.

But the real breakthrough that enabled today’s powerful models came from a particular architecture.

What Is a Transformer
Most modern LLMs are built using an architecture called the Transformer.

The Transformer architecture changed how models process sequences of information by introducing a mechanism called attention.

Instead of processing words one at a time in order like older neural networks, transformers can look at entire sequences of data simultaneously and determine which parts are most important.

This allows models to:

understand long-range relationships in text
maintain context across large inputs
generate more coherent and structured responses
Because of this design, transformers became the foundation for modern LLMs.

The Three Core Components of an LLM
At a simplified level, most LLM systems can be understood as a combination of three elements:

LLM = Data + Architecture + Training

Data
Large-scale datasets containing books, articles, code, conversations, and other forms of language.

This data teaches the model patterns of human communication and knowledge representation.

Architecture
The structural design of the model itself.

Today, this is primarily the Transformer architecture, which enables models to process complex relationships within data.

Training
The optimization process where the model learns to predict and generate language.

During training, the system adjusts millions or even billions of parameters to improve its predictions and outputs.

Together, these three components create the systems that power modern AI tools.

Engineering, AI, and the Missing Bridge
Coding, machine learning, and AI are incredibly valuable skills. But they were originally meant to complement engineering fundamentals, not replace them.

This observation raises an interesting thought.

What happens when we bring AI thinking back into engineering systems rather than keeping it limited to software?

Viewing AI from an Engineering Perspective
In engineering, almost every new technology eventually finds an entry point into physical systems.

So instead of thinking only about AI models running inside applications, another question emerges:

How can engineering systems and robotics systems behave or learn in ways similar to LLM-powered systems?

What would that actually look like?

Instead of only building software agents, we might begin designing systems where learning, decision-making, and adaptive behavior exist within engineered machines.

This idea goes beyond simple automation.

It begins to resemble something closer to an engineering model of intelligence.

From AI Agents to Intelligent Engineering Systems
Today, we often talk about AI agents in the context of software.

Agents can reason about tasks, interact with systems, retrieve information, and perform actions.

But if we extend this thinking into engineering systems, an interesting possibility appears.

Robotics systems and machines could potentially be designed to:

behave similarly to AI agents
integrate multiple data sources and signals
adapt to environments through learning
reason about tasks and actions
In this perspective, AI is no longer just a software capability.

It becomes something that can interact with and shape engineering systems directly.

This is where the worlds of AI, robotics, and engineering start to converge.

A Practical Question Builders Must Answer
As builders begin integrating AI systems into applications and infrastructure, another important question arises.

Should you build your own model, fine-tune an existing model, or use a pre-existing LLM?

Each approach has its own advantages and trade-offs.

Building Your Own Model
Provides maximum control and customization.

However, it requires extremely large datasets, significant computing resources, and specialized expertise.

Fine-Tuning an Existing Model
Allows developers to adapt a pre-trained model to a specific domain or problem.

This often provides a strong balance between performance and cost.

Using a Pre-Existing LLM
The fastest and most accessible option.

Most modern AI applications today rely on existing models and focus on building systems around them rather than training models from scratch.

The best choice depends on the complexity of the system, the resources available, and the specific problem being solved.

AI systems today are evolving rapidly.

But the most interesting opportunities may not only be in building larger models, but also in exploring how these systems integrate with real engineering systems and infrastructure.

Instead of thinking about AI purely as software, we can begin to ask:

How do intelligent models interact with machines, robotics systems, and the engineered world?

The future of AI may not only live in data centers and applications.

It may also live inside the systems we build to interact with the physical world.

Question for builders and engineers:

Is it better to build your own model for a system, fine-tune an existing model, or rely on pre-trained LLMs?