Tag: Software Engineering

  • The Diminishing Returns Of Vibe Coding

    The Diminishing Returns Of Vibe Coding

    I have used AI to build personal projects since before coding agents became a familiar tool. The early part can be great fun. You describe a feature, try the result and ask for the next thing. The uncomfortable moment comes later, when a small change sends you through files you barely remember creating.

    One failure mode I recognise is a codebase full of handlers, repositories, DTOs and interfaces that accomplishes very little. Each piece can look sensible on its own. Understanding why a request passes through all of them takes much longer than generating them did. That is where the return on another quick prompt starts to fall.

    What I mean by vibe coding

    Andrej Karpathy introduced the term in a February 2025 post describing a way of building where the code can recede from the developer’s attention. The part relevant to me is how much of the resulting program I can explain. I can use an agent heavily and still inspect the implementation, choose the boundaries and take responsibility for the result.

    The label becomes less useful when it is used to judge someone simply for using AI. I care more about a practical question: if this feature loses data or returns the wrong result tomorrow, can its owner trace the path and fix it? That can be difficult with handwritten code as well. Fast generation makes it easier to accumulate the problem before noticing it.

    A small example that reveals the cost

    Imagine adding an email-address change to an ASP.NET Core application backed by a relational database. The visible feature is a form and an endpoint. The actual decisions include who may change the address, whether it needs verification, how uniqueness is enforced and what happens when two requests arrive together.

    I would first read the existing authentication and user-update paths. Then I would ask the agent to implement a narrow change using those conventions, with an explicit list of files it expects to touch. If the repository already has a clear application-service pattern, following it is useful. If the feature suddenly needs a new generic repository framework, I want to understand the reason before accepting it.

    Two requests, one address

    A uniqueness check in application code is only part of the implementation. Two requests can both pass it before either commits. The database needs the appropriate unique constraint for the application’s chosen normalization rules, and the endpoint needs to translate a constraint conflict into a useful response. That is a review point I can test. A generated list of architecture patterns gives me less confidence.

    Give the agent boundaries it can work inside

    For that change, I would describe the accepted behaviour: an authenticated user can request a change to their own address, a duplicate is rejected, and an unverified new address does not accidentally receive privileges intended for a verified one. I would identify the existing persistence and verification components and ask for a short explanation of how the change uses them.

    The first patch should be small enough to read in one sitting. I would run the focused tests, inspect the migration and exercise the duplicate and unauthorized cases. If the feature requires a background verification email, I would review its failure handling separately. A database commit followed by a failed email send is a normal possibility that the workflow has to handle.

    I also want the agent to say which behaviour it has verified and which parts it has only inferred from reading the code. Passing tests are useful evidence within their scope. A new test that mocks away the database cannot demonstrate that a database uniqueness rule works.

    Notice when iteration stops helping

    Repeated fixes are a signal to pause. If every patch repairs one screen and breaks another, I would trace the data from input to storage to output and write down the intended state transitions. A quick diagram or a few concrete examples can expose a misunderstanding that more generated code would only spread.

    I would also look at the diff for duplication and unused paths. An agent may leave the old implementation in place while adding a new route that happens to satisfy the latest prompt. Searching for callers and running the existing regression tests helps establish which path the application actually uses.

    So are review, deployment and the next change. For a throwaway experiment, I can accept code I will never revisit. For a service that stores someone’s information, I need a smaller gap between what the code does and what I understand about it.

    The time spent reading is part of the development cost.

    A working rhythm I would use

    I would keep each change tied to one observable result, commit once that result is verified, and make the next request from that known state. When an agent proposes a broad refactor, I would separate it from the feature unless the feature actually depends on it. That makes failures easier to attribute and rollback easier to judge.

    AI is useful to me when it takes care of implementation work while leaving enough room to think about the application. The warning sign is the growing pile of decisions I have postponed understanding. Once that pile starts slowing every change, the productive next step is usually to read, simplify and test the code already there.

    For an EF Core implementation, the index documentation shows how to configure a unique index. I would still test the generated migration against the actual database provider and the application’s email-normalization rules.