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?

Understanding DHCP: How I Configured and Captured It in a GNS3 Lab

One of the best ways to truly understand a protocol is to build it yourself, watch the packets, and ask “why does each field exist?” This post walks through a DHCP lab I set up in GNS3 — configuring a Cisco router as the DHCP server, a switch in the middle, and two PCs as clients — then captures and explains every packet in the DORA handshake using Wireshark.

Lab topology
The setup is intentionally simple: one router acting as DHCP server, one switch forwarding traffic, and two VPCS clients requesting addresses.

R1
Router / DHCP server → SW1 Switch → PC1 Client·PC2 Client
Router configuration
On R1, I assigned a static IP to the interface facing the LAN, created a DHCP pool for the 192.168.1.0/24 subnet, and excluded a range so the router’s own address and a few reserved IPs wouldn’t be handed out.

interface GigabitEthernet1/0
ip address 192.168.1.254 255.255.255.0

ip dhcp pool LAN
network 192.168.1.0 255.255.255.0

ip dhcp excluded-address 192.168.1.2 192.168.1.10

The DORA handshake — explained packet by packet
DHCP works through four messages, collectively called DORA. I captured all four in Wireshark, and here’s exactly what each one contained and why.

1 — DISCOVER
Client broadcasts for a server

Source: 0.0.0.0 → Dest: 255.255.255.255 · UDP 68→67

Message type
Boot Request (1)
Client IP : 0.0.0.0
Client MAC : 00:50:79:66:68:01
Transaction ID : 0x7db99e22
Hops :0

2 — OFFER
Server proposes an IP address

Source: 192.168.1.254 → Dest: 192.168.1.1 · UDP 67→68

Message type :Boot Reply (2)
Your (client) IP :192.168.1.1
Server identifier :192.168.1.254
Subnet mask : 255.255.255.0
Options included :Router, DNS, Lease

3 — REQUEST
Client accepts the offer

Source: 0.0.0.0 → Dest: 255.255.255.255 · UDP 68→67

Message type : Boot Request (1)
Client IP : 192.168.1.1
Requested IP :192.168.1.1
Server ID : 192.168.1.254
Still broadcast?
Yes — 255.255.255.255

4 — ACK
Server confirms the lease

Source: 192.168.1.254 → Dest: 192.168.1.1 · UDP 67→68

Message type :Boot Reply (2)
Your (client) IP :192.168.1.1
Lease time :Option 51
Renewal time :Option 58
Rebind time :Option 59

Things I noticed in the capture
Why does the Request still broadcast, even after getting an Offer?
The client broadcasts the Request (instead of unicasting directly to the server) so that any other DHCP servers on the network also see it and know their offer was not accepted. This lets them release the IP they had tentatively reserved — a small but important detail that keeps address pools clean.

Why is 0.0.0.0 the source in Discover and Request?
The client doesn’t have an IP yet at this point — that’s the whole reason it’s sending DHCP messages. It uses 0.0.0.0 as a placeholder source, which the IP stack allows specifically for DHCP bootstrap situations.

What is the Transaction ID for?
The Transaction ID (0x7db99e22 in this capture) is a random number the client generates to match Offers and ACKs back to its own Discover and Request. This is important because in a busy network, multiple clients may be running DORA simultaneously — the Transaction ID tells each client which replies belong to them.

What about the Gratuitous ARP after the ACK?
Right after receiving the DHCP ACK, PC1 sent three Gratuitous ARP broadcasts for 192.168.1.1. This is the client announcing its new IP to the network and simultaneously checking whether any other device is already using that address. If something responds, there’s a conflict. If nothing responds, the address is safely claimed.

DHCP options visible in the Offer and ACK
Both the Offer and ACK carried the following options beyond just the IP address — this is what makes DHCP a full network configuration protocol, not just an IP assignment tool.

Options in DHCP :
From the Discover (Images 2 & 3):

Option 53 — DHCP Message Type (Discover)
Option 12 — Host Name
Option 61 — Client Identifier
Option 255 — End

From the Offer (Image 4):

Option 53 — DHCP Message Type (Offer)
Option 54 — DHCP Server Identifier (192.168.1.254)
Option 51 — IP Address Lease Time
Option 58 — Renewal Time Value
Option 59 — Rebinding Time Value
Option 1 — Subnet Mask (255.255.255.0)
Option 3 — Router
Option 6 — Domain Name Server
Option 255 — End

From the Request (Image 5):

Option 53 — DHCP Message Type (Request)
Option 54 — DHCP Server Identifier (192.168.1.254)
Option 50 — Requested IP Address (192.168.1.1)
Option 61 — Client Identifier
Option 12 — Host Name
Option 55 — Parameter Request List
Option 255 — End

From the ACK (Image 6):

Option 53 — DHCP Message Type (ACK)
Option 54 — DHCP Server Identifier (192.168.1.254)
Option 51 — IP Address Lease Time
Option 58 — Renewal Time Value
Option 59 — Rebinding Time Value
Option 1 — Subnet Mask (255.255.255.0)
Option 3 — Router
Option 6 — Domain Name Server
Option 255 — End
Result on the client
After the handshake completed, running ip dhcp on the VPCS client confirmed it received 192.168.1.11/24 with a gateway of 192.168.1.254 — as expected, since .1 through .10 were excluded from the pool.

What I took away from this lab
Reading about DHCP in a textbook and actually watching the four packets flow through Wireshark are very different experiences. Seeing the Transaction ID stay consistent across all four messages, watching the source IP go from 0.0.0.0 to the assigned address, and noticing the post-ACK Gratuitous ARPs made the protocol feel concrete rather than abstract. The next step is to add a second subnet and configure a DHCP relay (ip helper-address) to see how DORA behaves when the server and client are on different networks.