Tag: Technical Debt

  • 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.

  • Why Point to Point Integrations Break at Scale

    Why Point to Point Integrations Break at Scale

    A direct integration is often a reasonable first choice. If one system needs a small daily extract from another, a scheduled transfer can be easy to build and easy to explain. The trouble starts when more processes begin depending on that transfer and its original assumptions stay buried in the script.

    Consider a fictional seafood processor with an ERP, warehouse, production system, transport portal and reporting database. Sales uses warehouse stock to promise deliveries. Production updates the available lots. Quality approval determines which lots can ship. A stock export now carries several decisions that matter to people outside the warehouse team.

    Follow one stock update

    Suppose a PowerShell task exports stock every fifteen minutes and uploads a CSV to the ERP. At 18:00 on Friday the export fails. Both applications remain available, and the scheduler may even report success if the script caught an exception without returning a failing exit code. Sales keeps reading the last imported quantities.

    I would give that transfer a manifest containing an exportId, source timestamp, row count and checksum. The receiving job should record when it finished validating and applying the export.

    A successful file upload tells us about transport; we also need the import outcome before treating the stock as current.

    For a full snapshot, I would load into staging, validate the rows and publish the new snapshot atomically where the database design allows it. Replacing half the stock table before discovering a malformed row leaves readers with a mixture of two exports. A staging table also makes it easier to inspect rejected data without disturbing the last accepted snapshot.

    Make freshness part of the interface

    Freshness is part of the contract

    The stock response should carry an asOf timestamp. If the business agrees that stock older than thirty minutes is unsuitable for a delivery promise, the consuming application can flag it and ask for confirmation. Thirty minutes is an example threshold, not a universal integration rule. The warehouse and sales teams need to decide what delay they can tolerate.

    I would alert on the age of the last successfully applied export, with separate alerts for transfer failure and validation failure. This catches a job that never starts as well as one that crashes. Counting exceptions alone misses both a disabled scheduler and a script that keeps producing yesterday’s file.

    Changes expose the real dependencies

    A new warehouse version might change availableStock from individual units to cartons. The CSV still parses and all HTTP calls still return success. A contract test should catch that by checking representative items against agreed units and values. The same applies to cancelled orders, deleted items and time zones.

    With incremental extracts, I would document the cursor and deletion behaviour. A query based only on LastModified greater than the previous run time can miss records when timestamps collide or commits become visible late. A supported change feed is preferable where available. Otherwise the extraction design needs a stable ordering key, an overlap strategy and deduplication, plus a way to reconcile the full source periodically.

    This is where several independent scripts become expensive. Each one can implement a slightly different definition of stock, retry policy and cursor. A warehouse change then requires finding and testing every copy. A shared, versioned contract gives consumers a common interpretation even if several runtimes continue to carry the data.

    Count the dependencies you actually have

    For n systems, n × (n − 1) / 2 is the number of possible undirected pairs. Ten systems gives 45 pairs. That is an upper-bound illustration for a fully connected graph; it is not a prediction that every estate has 45 integrations. Direction, multiple contracts between the same systems and differing business rules complicate the count further.

    I would inventory the real flows instead. For each one, record the source, consumer, contract, schedule, credentials owner, last successful run and recovery method. Then ask which source changes require coordinated releases. That list is more useful for planning than a dramatic connection-count diagram.

    Improve the existing path before replacing it

    My first change would be to put the export code and its configuration template in version control, give it a maintained service identity and make failed runs fail visibly. Then I would add staging, freshness monitoring and a replay procedure using the original exportId. These changes can improve a scheduled transfer without a platform migration.

    A durable broker becomes useful when several consumers need independent retries, when the source must keep working during a target outage, or when the delay needs to be much shorter than the schedule. Each subscriber then needs its own progress and error handling. Moving the same undocumented payload onto a topic leaves the contract problem in place.

    I would start the migration with the flow causing the most operational pain. Disable its target during a test, restore it and check the recovered stock against the source. Repeat an export and verify that quantities stay correct. That gives a concrete basis for deciding which shared platform capabilities the next integration actually needs.