MoreRSS

site iconHackerNoonModify

We are an open and international community of 45,000+ contributing writers publishing stories and expertise for 4+ million curious and insightful monthly readers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of HackerNoon

The HackerNoon Newsletter: Should You Trust Your VPN Location? (1/11/2026)

2026-01-12 00:02:17

How are you, hacker?


🪐 What’s happening in tech today, January 11, 2026?


The HackerNoon Newsletter brings the HackerNoon homepage straight to your inbox. On this day, Insulin was first used to treat diabetes in humans in 1922, The oldest known living organism was discovered in 2013, Grand Canyon was declared a national monument in 1908, and we present you with these top quality stories. From Back to Basics: Database Design as Storytelling to How Supercell Powers its Massive Social Network with ScyllaDB, let’s dive right in.

How Supercell Powers its Massive Social Network with ScyllaDB


By @scylladb [ 6 Min read ] Supercell powers real-time cross-game chat, presence, and notifications for millions using ScyllaDB Cloud, enabling low-latency, scalable events. Read More.

Designing API Contracts for Legacy System Modernization


By @jamescaron [ 6 Min read ] A practical look at designing API contracts during legacy system modernization, focusing on real production failures and strategies to prevent silent regression Read More.

The Hidden Cost of AI: Why It’s Making Workers Smarter, but Organisations Dumber


By @yuliiaharkusha [ 8 Min read ] AI boosts individual performance but weakens organisational thinking. Why smarter workers and faster tools can leave companies less intelligent than before. Read More.

ISO 27001 Compliance Tools in 2026: A Comparative Overview of 7 Leading Platforms


By @stevebeyatte [ 7 Min read ] Breaking down the best ISO 27001 Compliance tools in the market for 2026. Read More.

IPv6 and CTV: The Measurement Challenge From the Fastest-Growing Ad Channel


By @ipinfo [ 7 Min read ] IPv6 breaks digital ad measurement. Learn how IPinfo’s research-driven, active-measurement model restores accuracy across CTV and all channels. Read More.

Back to Basics: Database Design as Storytelling


By @dataops [ 3 Min read ] Why great database design is really storytelling—and why ignoring relational fundamentals leads to poor performance AI can’t fix. Read More.

Should You Trust Your VPN Location?


By @ipinfo [ 9 Min read ] IPinfo reveals how most VPNs misrepresent locations and why real IP geolocation requires active measurement, not claims. Read More.

Secury Wallet Unveils Next-Generation Multichain Crypto Wallet With Chat to Pay, Opens $SEC Presale


By @btcwire [ 2 Min read ] The project has opened the $SEC token presale, including early staking opportunities offering up to 100% APY during the presale phase. Chat to Pay is an instant Read More.


🧑‍💻 What happened in your world this week?

It's been said that writing can help consolidate technical knowledge, establish credibility, and contribute to emerging community standards. Feeling stuck? We got you covered ⬇️⬇️⬇️


ANSWER THESE GREATEST INTERVIEW QUESTIONS OF ALL TIME


We hope you enjoy this worth of free reading material. Feel free to forward this email to a nerdy friend who'll love you for it.See you on Planet Internet! With love, The HackerNoon Team ✌️


Go: The Testing/Synctest Package Explained

2026-01-12 00:00:09

In Go 1.24, we introduced the testing/synctest package as an experimental package. This package can significantly simplify writing tests for concurrent, asynchronous code. In Go 1.25, the testing/synctest package has graduated from experiment to general availability.

\ What follows is the blog version of my talk on the testing/synctest package at GopherCon Europe 2025 in Berlin.

What is an asynchronous function?

A synchronous function is pretty simple. You call it, it does something, and it returns.

\ An asynchronous function is different. You call it, it returns, and then it does something.

\ As a concrete, if somewhat artificial, example, the following Cleanup function is synchronous. You call it, it deletes a cache directory, and it returns.

func (c *Cache) Cleanup() {
    os.RemoveAll(c.cacheDir)
}

CleanupInBackground is an asynchronous function. You call it, it returns, and the cache directory is deleted…sooner or later.

func (c *Cache) CleanupInBackground() {
    go os.RemoveAll(c.cacheDir)
}

\ Sometimes an asynchronous function does something in the future. For example, the context package’s WithDeadline function returns a context which will be canceled in the future.

package context

// WithDeadline returns a derived context
// with a deadline no later than d.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

\ When I talk about testing concurrent code, I mean testing these sorts of asynchronous operations, both ones which use real time and ones which do not.

Tests

A test verifies that a system behaves as we expect. There’s a lot of terminology describing types of test–unit tests, integration tests, and so on–but for our purposes here every kind of test reduces to three steps:

  1. Set up some initial conditions.
  2. Tell the system under test to do something.
  3. Verify the result.

\ Testing a synchronous function is straightforward:

  • You call the function;
  • the function does something and returns;
  • you verify the result.

\ Testing an asynchronous function, however, is tricky:

  • You call the function;
  • it returns;
  • you wait for it to finish doing whatever it does;
  • you verify the result.

\ If you don’t wait for the correct amount of time, you may find yourself verifying the result of an operation that hasn’t happened yet or has only happened partially. This never ends well.

\ Testing an asynchronous function is especially tricky when you want to assert that something has not happened. You can verify that the thing has not happened yet, but how do you know with certainty that it isn’t going to happen later?

An example

To make things a little more concrete, let’s work with a real-world example. Consider the context package’s WithDeadline function again.

package context

// WithDeadline returns a derived context
// with a deadline no later than d.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

\ There are two obvious tests to write for WithDeadline.

  1. The context is not canceled before the deadline.
  2. The context is canceled after the deadline.

\ Let’s write a test.

\ To keep the amount of code marginally less overwhelming, we’ll just test the second case: After the deadline expires, the context is canceled.

func TestWithDeadlineAfterDeadline(t *testing.T) {
    deadline := time.Now().Add(1 * time.Second)
    ctx, _ := context.WithDeadline(t.Context(), deadline)

    time.Sleep(time.Until(deadline))

    if err := ctx.Err(); err != context.DeadlineExceeded {
        t.Fatalf("context not canceled after deadline")
    }
}

\ This test is simple:

  1. Use context.WithDeadline to create a context with a deadline one second in the future.
  2. Wait until the deadline.
  3. Verify that the context is canceled.

\ Unfortunately, this test obviously has a problem. It sleeps until the exact moment the deadline expires. Odds are good that the context has not been canceled yet by the time we examine it. At best, this test will be very flaky.

\ Let’s fix it.

time.Sleep(time.Until(deadline) + 100*time.Millisecond)

We can sleep until 100ms after the deadline. A hundred milliseconds is an eternity in computer terms. This should be fine.

\ Unfortunately, we still have two problems.

\ First, this test takes 1.1 seconds to execute. That’s slow. This is a simple test. It should execute in milliseconds at the most.

\ Second, this test is flaky. A hundred milliseconds is an eternity in computer terms, but on an overloaded continuous integration (CI) system it isn’t unusual to see pauses much longer than that. This test will probably pass consistently on a developer’s workstation, but I would expect occasional failures in a CI system.

Slow or flaky: Pick two

Tests that use real time are always slow or flaky. Usually they’re both. If the test waits longer than necessary, it is slow. If it doesn’t wait long enough, it is flaky. You can make the test more slow and less flaky, or less slow and more flaky, but you can’t make it fast and reliable.

\ We have a lot of tests in the net/http package which use this approach. They’re all slow and/or flaky, which is what started me down the road which brings us here today.

Write synchronous functions?

The simplest way to test an asynchronous function is not to do it. Synchronous functions are easy to test. If you can transform an asynchronous function into a synchronous one, it will be easier to test.

\ For example, if we consider our cache cleanup functions from earlier, the synchronous Cleanup is obviously better than the asynchronous CleanupInBackground. The synchronous function is easier to test, and the caller can easily start a new goroutine to run it in the background if needed. As a general rule, the higher up the call stack you can push your concurrency, the better.

// CleanupInBackground is hard to test.
cache.CleanupInBackground()

// Cleanup is easy to test,
// and easy to run in the background when needed.
go cache.Cleanup()

\ Unfortunately, this sort of transformation isn’t always possible. For example, context.WithDeadline is an inherently asynchronous API.

Instrument code for testability?

A better approach is to make our code more testable.

\ Here’s an example of what this might look like for our WithDeadline test:

func TestWithDeadlineAfterDeadline(t *testing.T) {
    clock := fakeClock()
    timeout := 1 * time.Second
    deadline := clock.Now().Add(timeout)

    ctx, _ := context.WithDeadlineClock(
        t.Context(), deadline, clock)

    clock.Advance(timeout)
    context.WaitUntilIdle(ctx)
    if err := ctx.Err(); err != context.DeadlineExceeded {
        t.Fatalf("context not canceled after deadline")
    }
}

\ Instead of using real time, we use a fake time implementation. Using fake time avoids unnecessarily slow tests, because we never wait around doing nothing. It also helps avoid test flakiness, since the current time only changes when the test adjusts it.

\ There are various fake time packages out there, or you can write your own.

\ To use fake time, we need to modify our API to accept a fake clock. I’ve added a context.WithDeadlineClock function here, that takes an additional clock parameter:

ctx, _ := context.WithDeadlineClock(
    t.Context(), deadline, clock)

\ When we advance our fake clock, we have a problem. Advancing time is an asynchrounous operation. Sleeping goroutines may wake up, timers may send on their channels, and timer functions may run. We need to wait for that work to finish before we can test the expected behavior of the system.

\ I’ve added a context.WaitUntilIdle function here, which waits for any background work related to a context to complete:

clock.Advance(timeout)
context.WaitUntilIdle(ctx)

\ This is a simple example, but it demonstrates the two fundamental principles of writing testable concurrent code:

  1. Use fake time (if you use time).
  2. Have some way to wait for quiescence, which is a fancy way of saying “all background activity has stopped and the system is stable”.

\ The interesting question, of course, is how we do this. I’ve glossed over the details in this example because there are some big downsides to this approach.

\ It’s hard. Using a fake clock isn’t difficult, but identifying when background concurrent work is finished and it is safe to examine the state of the system is.

\ Your code becomes less idiomatic. You can’t use standard time package functions. You need to be very careful to keep track of everything happening in the background.

\ You need to instrument not just your code, but any other packages you use. If you call any third-party concurrent code, you’re probably out of luck.

\ Worst of all, it can be just about impossible to retrofit this approach into an existing codebase.

\ I attempted to apply this approach to Go’s HTTP implementation, and while I had some success at doing so in places, the HTTP/2 server simply defeated me. In particular, adding instrumentation to detect quiescence without extensive rewriting proved infeasible, or at least beyond my skills.

Horrible runtime hacks?

What do we do if we can’t make our code testable?

\ What if instead of instrumenting our code, we had a way to observe the behavior of the uninstrumented system?

\ \ A Go program consists of a set of goroutines. Those goroutines have states. We just need to wait until all the goroutines have stopped running.

\ Unfortunately, the Go runtime doesn’t provide any way to tell what those goroutines are doing. Or does it?

\ The runtime package contains a function that gives us a stack trace for every running goroutine, as well as their states. This is text intended for human consumption, but we could parse that output. Could we use this to detect quiescence?

\ Now, of course this is a terrible idea. There is no guarantee that the format of these stack traces will be stable over time. You should not do this.

\ I did it. And it worked. In fact, it worked surprisingly well.

\ With a simple implementation of a fake clock, a small amount of instrumentation to keep track of what goroutines were part of the test, and some horrifying abuse of runtime.Stack, I finally had a way to write fast, reliable tests for the http package.

\ The underlying implementation of these tests was horrible, but it demonstrated that there was a useful concept here.

A better way

Go may have built-in concurrency, but testing programs that use that concurrency is hard.

\ We’re faced with an unfortunate choice: We can write simple, idiomatic code, but it will be impossible to test quickly and reliably; or we can write testable code, but it will be complicated and unidiomatic.

\ So we asked ourselves what we can do to make this better.

\ As we saw earlier, the two fundamental features required to write testable concurrent code are fake time and a way to wait for quiescence.

\ We need a better way to to wait for quiescence. We should be able to ask the runtime when background goroutines have finished their work. We also want to be able to limit the scope of this query to a single test, so that unrelated tests do not interfere with each other.

\ We also need better support for testing programs using fake time.

\ It isn’t hard to make a fake time implementation, but code which uses an implementation like this is not idiomatic.

\ Idiomatic code will use a time.Timer, but it is not possible to create a fake Timer. We asked ourselves whether we should provide a way for tests to create a fake Timer, where the test controls when the timer fires.

\ A testing implementation of time needs to define an entirely new version of the time package, and pass that to every function that operates on time. We considered whether we should define a common time interface, in the same way that net.Conn is a common interface describing a network connection.

\ What we realized, however, is that unlike network connections, there is only one possible implementation of fake time. A fake network may want to introduce latency or errors. Time, in contrast, does only one thing: It moves forward. Tests need to control the rate at which time progresses, but a timer scheduled to fire ten seconds in the future should always fire ten (possibly fake) seconds in the future.

\ In addition, we don’t want to upset the entire Go ecosystem. Most programs today use functions in the time package. We want to keep those programs not only working, but idiomatic.

\ This led to the conclusion that what we need is a way for a test to tell the time package to use a fake clock, in much the same way that the Go playground uses a fake clock. Unlike the playground, we need to limit the scope of that change to a single test. (It may not be obvious that the Go playground uses a fake clock, because we turn any fake delays into real delays on the front end, but it does.)

The synctest experiment

And so in Go 1.24 we introduced testing/synctest, a new, experimental package to simplify testing concurrent programs. Over the months following the release of Go 1.24 we gathered feedback from early adopters. (Thank you to everyone who tried it out!) We made a number of changes to address problems and shortcomings. And now, in Go 1.25, we’ve released the testing/synctest package as part of the standard library.

\ It lets you run a function in what we’re calling a “bubble”. Within the bubble, the time package uses a fake clock, and the synctest package provides a function to wait for the bubble to quiesce.

The synctest package

The synctest package contains just two functions.

package synctest

// Test executes f in a new bubble.
// Goroutines in the bubble use a fake clock.
func Test(t *testing.T, f func(*testing.T))

// Wait waits for background activity in the bubble to complete.
func Wait()

Test executes a function in a new bubble.

\ Wait blocks until every goroutine in the bubble is blocked waiting for some other goroutine in the bubble. We call that state being “durably blocked”.

Testing with synctest

Let’s look at an example of synctest in action.

func TestWithDeadlineAfterDeadline(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        deadline := time.Now().Add(1 * time.Second)
        ctx, _ := context.WithDeadline(t.Context(), deadline)

        time.Sleep(time.Until(deadline))
        synctest.Wait()
        if err := ctx.Err(); err != context.DeadlineExceeded {
            t.Fatalf("context not canceled after deadline")
        }
    })
}

\ This might look a little familiar. This is the naïve test for context.WithDeadline that we looked at earlier. The only changes are that we’ve wrapped the test in a synctest.Test call to execute it in a bubble and we have added a synctest.Wait call.

\ This test is fast and reliable. It runs almost instantaneously. It precisely tests the expected behavior of the system under test. It also requires no modification of the context package.

\ Using the synctest package, we can write simple, idiomatic code and test it reliably.

\ This is a very simple example, of course, but this is a real test of real production code. If synctest had existed when the context package was written, we would have had a much easier time writing tests for it.

Time

Time in the bubble behaves much the same as the fake time in the Go playground. Time starts at midnight, January 1, 2000 UTC. If you need to run a test at some specific point in time for some reason, you can just sleep until then.

func TestAtSpecificTime(t *testing.T) {
   synctest.Test(t, func(t *testing.T) {
       // 2000-01-01 00:00:00 +0000 UTC
       t.Log(time.Now().In(time.UTC))

       // This does not take 25 years.
       time.Sleep(time.Until(
           time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)))

       // 2025-01-01 00:00:00 +0000 UTC
       t.Log(time.Now().In(time.UTC))
   })
}

\ Time only passes when every goroutine in the bubble has blocked. You can think of the bubble as simulating an infinitely fast computer: Any amount of computation takes no time.

\ The following test will always print that zero seconds of fake time have elapsed since the start of the test, no matter how much real time has passed.

func TestExpensiveWork(t *testing.T) {
   synctest.Test(t, func(t *testing.T) {
       start := time.Now()
       for range 1e7 {
           // do expensive work
       }
       t.Log(time.Since(start)) // 0s
   })
}

\ In the next test, the time.Sleep call will return immediately, rather than waiting for ten real seconds. The test will always print that exactly ten fake seconds have passed since the start of the test.

func TestSleep(t *testing.T) {
   synctest.Test(t, func(t *testing.T) {
       start := time.Now()
       time.Sleep(10 * time.Second)
       t.Log(time.Since(start)) // 10s
   })
}

Waiting for quiescence

The synctest.Wait function lets us wait for background activity to complete.

func TestWait(t *testing.T) {
   synctest.Test(t, func(t *testing.T) {
       done := false
       go func() {
           done = true
       }()

       // Wait for the above goroutine to finish.
       synctest.Wait()

       t.Log(done) // true
   })
}

\ If we didn’t have the Wait call in the above test, we would have a race condition: One goroutine modifies the done variable while another reads from it without synchronization. The Wait call provides that synchronization.

\ You may be familiar with the -race test flag, which enables the data race detector. The race detector is aware of the synchronization provided by Wait, and does not complain about this test. If we forgot the Wait call, the race detector would correctly complain.

\ The synctest.Wait function provides synchronization, but the passage of time does not.

\ In the next example, one goroutine writes to the done variable while another sleeps for one nanosecond before reading from it. It should be obvious that when run with a real clock outside a synctest bubble, this code contains a race condition. Inside a synctest bubble, while the fake clock ensures that the goroutine completes before time.Sleep returns, the race detector will still report the data race, just like it would if this code were run outside a synctest bubble.

func TestTimeDataRace(t *testing.T) {
   synctest.Test(t, func(t *testing.T) {
       done := false
       go func() {
           done = true // write
       }()

       time.Sleep(1 * time.Nanosecond)

       t.Log(done)     // read (unsynchronized)
   })
}

\ Adding a Wait call provides explicit synchronization and fixes the data race:

time.Sleep(1 * time.Nanosecond)
synctest.Wait() // synchronize
t.Log(done)     // read

Example: io.Copy

Taking advantage of the synchronization provided by synctest.Wait allows us to write simpler tests with less explicit synchronization.

\ For example, consider this test of io.Copy.

func TestIOCopy(t *testing.T) {
   synctest.Test(t, func(t *testing.T) {
       srcReader, srcWriter := io.Pipe()
       defer srcWriter.Close()

       var dst bytes.Buffer
       go io.Copy(&dst, srcReader)

       data := "1234"
       srcWriter.Write([]byte("1234"))
       synctest.Wait()

       if got, want := dst.String(), data; got != want {
           t.Errorf("Copy wrote %q, want %q", got, want)
       }
   })
}

\ The io.Copy function copies data from an io.Reader to an io.Writer. You might not immediately think of io.Copy as a concurrent function, since it blocks until the copy has completed. However, providing data to io.Copy’s reader is an asynchronous operation:

  • Copy calls the reader’s Read method;
  • Read returns some data;
  • and the data is written to the writer at a later time.

\ In this test, we are verifying that io.Copy writes new data to the writer without waiting to fill its buffer.

\ Looking at the test step by step, we first create an io.Pipe to serve as the source io.Copy reads from:

srcReader, srcWriter := io.Pipe()
defer srcWriter.Close()

\ We call io.Copy in a new goroutine, copying from the read end of the pipe into a bytes.Buffer:

var dst bytes.Buffer
go io.Copy(&dst, srcReader)

\ We write to the other end of the pipe, and wait for io.Copy to handle the data:

data := "1234"
srcWriter.Write([]byte("1234"))
synctest.Wait()

\ Finally, we verify that the destination buffer contains the desired data:

if got, want := dst.String(), data; got != want {
    t.Errorf("Copy wrote %q, want %q", got, want)
}

\ We don’t need to add a mutex or other synchronization around the destination buffer, because synctest.Wait ensures that it is never accessed concurrently.

\ This test demonstrates a few important points.

\ Even synchronous functions like io.Copy, which do not perform additional background work after they return, may exhibit asynchronous behaviors.

\ Using synctest.Wait, we can test those behaviors.

\ Note also that this test does not work with time. Many asynchronous systems involve time, but not all.

Bubble exit

The synctest.Test function waits for all goroutines in the bubble to exit before returning. Time stops advancing after the root goroutine (the goroutine started by Test) returns.

\ In the next example, Test waits for the background goroutine to run and exit before it returns:

func TestWaitForGoroutine(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        go func() {
            // This runs before synctest.Test returns.
        }()
    })
}

\ In this example, we schedule a time.AfterFunc for a time in the future. The bubble’s root goroutine returns before that time is reached, so the AfterFunc never runs:

func TestDoNotWaitForTimer(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        time.AfterFunc(1 * time.Nanosecond, func() {
            // This never runs.
        })
    })
}

\ In the next example, we start a goroutine that sleeps. The root goroutine returns and time stops advancing. The bubble is now deadlocked, because Test is waiting for all goroutines in the bubble to finish but the sleeping goroutine is waiting for time to advance.

func TestDeadlock(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        go func() {
            // This sleep never returns and the test deadlocks.
            time.Sleep(1 * time.Nanosecond)
        }()
    })
}

Deadlocks

The synctest package panics when a bubble is deadlocked due to every goroutine in the bubble being durably blocked on another goroutine in the bubble.

--- FAIL: Test (0.00s)
--- FAIL: TestDeadlock (0.00s)
panic: deadlock: main bubble goroutine has exited but blocked goroutines remain [recovered, repanicked]

goroutine 7 [running]:
(stacks elided for clarity)

goroutine 10 [sleep (durable), synctest bubble 1]:
time.Sleep(0x1)
    /Users/dneil/src/go/src/runtime/time.go:361 +0x130
_.TestDeadlock.func1.1()
    /tmp/s/main_test.go:13 +0x20
created by _.TestDeadlock.func1 in goroutine 9
    /tmp/s/main_test.go:11 +0x24
FAIL    _   0.173s
FAIL

\ The runtime will print stack traces for every goroutine in the deadlocked bubble.

\ When printing the status of a bubbled goroutine, the runtime indicates when the goroutine is durably blocked. You can see that the sleeping goroutine in this test is durably blocked.

Durable blocking

“Durably blocking” is a core concept in synctest.

\ A goroutine is durably blocked when it is not only blocked, but when it can only be unblocked by another goroutine in the same bubble.

\ When every goroutine in a bubble is durably blocked:

  1. synctest.Wait returns.
  2. If there is no synctest.Wait call in progress, fake time advances instantly to the next point that will wake a goroutine.
  3. If there is no goroutine that can be woken by advancing time, the bubble is deadlocked and the test fails.

\ It is important for us to make a distinction between a goroutine which is merely blocked and one which is durably blocked. We don’t want to declare a deadlock when a goroutine is temporarily blocked on some event arising outside its bubble.

\ Let’s look at some ways in which a goroutine can block non-durably.

Not durably blocking: I/O (files, pipes, network connections, etc.)

The most important limitation is that I/O is not durably blocking, including network I/O. A goroutine reading from a network connection may be blocked, but it will be unblocked by data arriving on that connection.

\ This is obviously true for a connection to some network service, but it is also true for a loopback connection, even when the reader and writer are both in the same bubble.

\ When we write data to a network socket, even a loopback socket, the data is passed to the kernel for delivery. There is a period of time between the write system call returning and the kernel notifying the other side of the connection that data is available. The Go runtime cannot distinguish between a goroutine blocked waiting for data that is already in the kernel’s buffers and one blocked waiting for data that will not arrive.

\ This means that tests of networked programs using synctest usually cannot use real network connections. Instead, they should use an in-memory fake.

\ I’m not going to go over the process of creating a fake network here, but the synctest package documentation contains a complete worked example of testing an HTTP client and server communicating over a fake network.

Not durably blocking: syscalls, cgo calls, anything that isn’t Go

Syscalls and cgo calls are not durably blocking. We can only reason about the state of goroutines executing Go code.

Not durably blocking: Mutexes

Perhaps surprisingly, mutexes are not durably blocking. This is a decision born of practicality: Mutexes are often used to guard global state, so a bubbled goroutine will often need to acquire a mutex held outside its bubble. Mutexes are highly performance-sensitive, so adding additional instrumentation to them risks slowing down non-test programs.

\ We can test programs that use mutexes with synctest, but the fake clock will not advance while a goroutine is blocked on mutex acquisition. This hasn’t posed a problem in any case we’ve encountered, but it is something to be aware of.

Durably blocking: time.Sleep

So what is durably blocking?

\ time.Sleep is obviously durable, since time can only advance when every goroutine in the bubble is durably blocked.

Durably blocking: send or receive on channels created in the same bubble

Channel operations on channels created within the same bubble are durable.

\ We make a distinction between bubbled channels (created in a bubble) and unbubbled channels (created outside any bubble). This means that a function using a global channel for synchronization, for example to control access to a globally cached resource, can be safely called from within a bubble.

\ Trying to operate on a bubbled channel from outside its bubble is an error.

Durably blocking: sync.WaitGroup belonging to the same bubble

We also associate sync.WaitGroups with bubbles.

\ WaitGroup doesn’t have a constructor, so we make the association with the bubble implicitly on the first call to Go or Add.

\ As with channels, waiting on a WaitGroup belonging to the same bubble is durably blocking, and waiting on one from outside the bubble is not. Calling Go or Add on a WaitGroup belonging to a different bubble is an error.

Durably blocking: sync.Cond.Wait

Waiting on a sync.Cond is always durably blocking. Waking up a goroutine waiting on a Cond in a different bubble is an error.

Durably blocking: select{}

Finally, an empty select is durably blocking. (A select with cases is durably blocking if all the operations in it are so.)

\ That’s the complete list of durably blocking operations. It isn’t very long, but it’s enough to handle almost all real-world programs.

\ The rule is that a goroutine is durably blocked when it is blocked, and we can guarantee that it can only be unblocked by another goroutine in its bubble.

\ In cases where it is possible to attempt to wake a bubbled goroutine from outside its bubble, we panic. For example, it is an error to operate on a bubbled channel from outside its bubble.

Changes from 1.24 to 1.25

We released an experimental version of the synctest package in Go 1.24. To ensure that early adopters were aware of the experimental status of the package, you needed to set a GOEXPERIMENT flag to make the package visible.

\ The feedback we received from those early adopters was invaluable, both in demonstrating that the package is useful and in uncovering areas where the API needed work.

\ These are some of the changes made between the experimental version and the version released in Go 1.25.

Replaced Run with Test

The original version of the API created a bubble with a Run function:

// Run executes f in a new bubble.
func Run(f func())

\ It became clear that we needed a way to create a *testing.T that is scoped to a bubble. For example, t.Cleanup should run cleanup functions in the same bubble they are registered in, not after the bubble exits. We renamed Run to Test and made it create a T scoped to the lifetime of the new bubble.

Time stops when a bubble’s root goroutine returns

We originally continued to advance time within a bubble for so long as the bubble contained any goroutines waiting for future events. This turned out to be very confusing when a long-lived goroutine never returned, such as a goroutine reading forever from a time.Ticker. We now stop advancing time when a bubble’s root goroutine returns. If the bubble is blocked waiting for time to advance, this results in a deadlock and a panic which can be analyzed.

Removed cases where “durable” wasn’t

We cleaned up the definition of “durably blocking”. The original implementation had cases where a durably blocked goroutine could be unblocked from outside the bubble. For example, channels recorded whether they were created in a bubble, but not which in which bubble they were created, so one bubble could unblock a channel in a different bubble. The current implementation contains no cases we know of where a durably blocked goroutine can be unblocked from outside its bubble.

Better stack traces

We made improvements to the information printed in stack traces. When a bubble deadlocks, we by default now only print stacks for the goroutines in that bubble. Stack traces also clearly indicate which goroutines in a bubble are durably blocked.

Randomized events happening at the same time

We made improvements to the randomization of events happening at the same time. Originally, timers scheduled to fire at the same instant would always do so in the order they were created. This ordering is now randomized.

Future work

We’re pretty happy with the synctest package at the moment.

\ Aside from the inevitable bug fixes, we don’t currently expect any major changes to it in the future. Of course, with wider adoption it is always possible that we’ll discover something that needs doing.

\ One possible area of work is to improve the detection of durably blocked goroutines. It would be nice if we could make mutex operations durably blocking, with a restriction that a mutex acquired in a bubble must be released from within the same bubble.

\ Testing networked code with synctest requires a fake network. The net.Pipe function can create a fake net.Conn, but there is currently no standard library function that creates a fake net.Listener or net.PacketConn. In addition, the net.Conn returned by net.Pipe is synchronous–every write blocks until a read consumes the data–which is not representative of real network behavior. Perhaps we should add a good fake implementations of common network interfaces to the standard library.

Conclusion

That’s the synctest package.

\ I can’t say that it makes testing concurrent code simple, because concurrency is never simple. What it does is let you write the simplest possible concurrent code, using idiomatic Go, and the standard time package, and then write fast, reliable tests for it.

\ I hope you find it useful.


Damien Neil

\ This article is available on The Go Blog under a CC BY 4.0 DEED license.

\ Photo by Mildlee on Unsplash

The TechBeat: I Saw a Phishing Site That Traps Security Bots (1/11/2026)

2026-01-11 15:10:56

How are you, hacker? 🪐Want to know what's trending right now?: The Techbeat by HackerNoon has got you covered with fresh content from our trending stories of the day! Set email preference here. ## ISO 27001 Compliance Tools in 2026: A Comparative Overview of 7 Leading Platforms By @stevebeyatte [ 7 Min read ] Breaking down the best ISO 27001 Compliance tools in the market for 2026. Read More.

The Hidden Cost of AI: Why It’s Making Workers Smarter, but Organisations Dumber

By @yuliiaharkusha [ 8 Min read ] AI boosts individual performance but weakens organisational thinking. Why smarter workers and faster tools can leave companies less intelligent than before. Read More.

A Developer's Guide to Building Next-Gen Smart Wallets With ERC-4337 — Part 2: Bundlers

By @hacker39947670 [ 15 Min read ] Bundlers are the bridge between account abstraction and the execution layer. Read More.

IPv6 and CTV: The Measurement Challenge From the Fastest-Growing Ad Channel

By @ipinfo [ 7 Min read ] IPv6 breaks digital ad measurement. Learn how IPinfo’s research-driven, active-measurement model restores accuracy across CTV and all channels. Read More.

Should We Be Worried About Losing Jobs? Or Just Adapt Our Civilization to New Reality?

By @chris127 [ 10 Min read ] The question isn't whether jobs will disappear—it's whether our traditional work model is still valid. Read More.

How Crypto Can Protect People from Currency Wars

By @chris127 [ 8 Min read ] When we think of war, we imagine soldiers, weapons, and physical destruction. But there's another type of war that affects millions of people worldwide… Read More.

The Realistic Guide to Mastering AI Agents in 2026

By @paoloap [ 12 Min read ] Master AI agents in 6-9 months with this complete learning roadmap. From math foundations to deploying production systems, get every resource you need. Read More.

Cursor’s Graphite Deal Aims to Close the Loop From Writing Code to Merging It

By @ainativedev [ 3 Min read ] Cursor’s acquisition of Graphite aims to unify code creation and review, and in the process brings the company closer to territory long dominated by GitHub. Read More.

Should You Trust Your VPN Location?

By @ipinfo [ 9 Min read ] IPinfo reveals how most VPNs misrepresent locations and why real IP geolocation requires active measurement, not claims. Read More.

Prompt Chaining: Turn One Prompt Into a Reliable LLM Workflow

By @superorange0707 [ 7 Min read ] Prompt Chaining links prompts into workflows—linear, branching, looping—so LLM outputs are structured, debuggable, and production-ready. Read More.

Can ChatGPT Outperform the Market? Week 23

By @nathanbsmith729 [ 4 Min read ] Another strong week… Read More.

What Comes After Growth Hacks: AI-Driven Marketing Systems

By @khamisihamisi [ 3 Min read ] What comes after growth hacks isn’t more hustle. Its systems and those systems are powered by AI. Read More.

Meet ScyllaDB: HackerNoon Company of the Week

By @companyoftheweek [ 4 Min read ] Meet ScyllaDB, the high-performance NoSQL database delivering predictable millisecond latencies for Discord and hundreds more. Read More.

Why Ledger's Latest Data Breach Exposes the Hidden Risks of Third-Party Dependencies

By @ishanpandey [ 3 Min read ] Ledger data breach via Global-e exposes customer info. No crypto stolen, but phishing attempts surge. Third-party risks examined. Read More.

The Next Big Thing Isn’t on Your Phone. It’s AI-Powered XR and It’s Already Taking Over. Part II

By @romanaxelrod [ 7 Min read ] AI-powered XR won’t be won by smart glasses alone. Why Big Tech is stuck optimizing and how deep tech, AI-driven R&D, and new materials are reshaping computing Read More.

Code Smell 12 - Null is Schizophrenic and Does Not Exist in The Real-world

By @mcsee [ 5 Min read ] Programmers use Null as different flags. It can hint at an absence, an undefined value, en error etc. Multiple semantics lead to coupling and defects. Read More.

5 Gen Z Marketing Trends That Will Make or Break Brands in 2026

By @lomitpatel [ 4 Min read ] Discover 5 Gen Z marketing trends for 2026. Learn how brands can build community, use AI transparently, and engage Gen Z with culture and authenticity. Read More.

Write Symfony Commands Like You Write Controllers—Finally

By @mattleads [ 12 Min read ] Symfony 7.4 makes Console commands expressive and type-safe. Read More.

I Saw a Phishing Site That Traps Security Bots

By @behindthesurface [ 5 Min read ] How modern phishing kits use honeypots, cloaking, and adversary-in-the-middle attacks—and how defenders can turn those same tactics against them. Read More.

Anyone Can Be a Victim to a Phishing Scam. Here’s My Story.

By @linh [ 5 Min read ] Got a call from 888-373-1969 claiming to be the Chase fraud department? Trust but verify should be your principle to avoid phishing scam! Read More. 🧑‍💻 What happened in your world this week? It's been said that writing can help consolidate technical knowledge, establish credibility, and contribute to emerging community standards. Feeling stuck? We got you covered ⬇️⬇️⬇️ ANSWER THESE GREATEST INTERVIEW QUESTIONS OF ALL TIME We hope you enjoy this worth of free reading material. Feel free to forward this email to a nerdy friend who'll love you for it. See you on Planet Internet! With love, The HackerNoon Team ✌️

Why “Accuracy” Fails for Uplift Models (and What to Use Instead)

2026-01-11 04:00:12

When it comes to uplift modeling, traditional performance metrics commonly used for other machine learning tasks may fall short.

The standard machine learning algorithms / business cases learn on the training data, predict the target on the test data and compare it to the ground truth.

However, in uplift modeling, the concept of ground truth becomes elusive since we cannot observe the impact of being treated and not treated on an individual simultaneously.

How to choose validation dataset ?

The choice of data for training and testing an uplift model depends on the available information and the specific context.

Uplift models are commonly used for marketing campaigns. Let’s illustrate how validation data is chosen from this perspective.

If we have a single campaign, we can divide the customers within that campaign into training and validation sets.

One campaign situation

However, if there are multiple campaigns available, we can utilize some campaigns for training the model and reserve others for validation. This strategy allows the model to learn from a broader range of scenarios and potentially improves its generalization capabilities.

Multiple campaigns dataset

Data used for uplift modeling should have sufficient information about the treatment, control groups, and relevant covariates.

Without these essential components, accurately capturing uplift becomes challenging.

The main approaches

There are two main ways to assess the performance of an uplift model: Cumulative Gain and Qini. Let’s explore them:

Cumulative Gain :

The cumulative gain illustrates the incremental response rate or outcome achieved by targeting a specific percentage of the population.

To calculate cumulative gain, the individuals are ranked based on their uplift scores, and the sorted list is divided into a series of equal-sized deciles or percentile groups. The cumulative gain is then computed by summing up the outcomes or responses of individuals within each group.

Cumulative Gain Formula

N : number of clients for control (C) and treatment (T) groups for the first p% of the clients

Y : Sum of our uplift in a metric we chose for control (C) and treatment (T) groups for the first p% of the clients

For instance, CG at 20% of population targeted corresponds to the total incremental gain if we treat only the instances with top 20% highest scores.

In the example provided below, we observe that targeting the top 20% of clients with the highest scores yields a cumulative gain of 0.019.

Cumulative Gain Plot.

A steeper curve indicates a better model, as it shows that a higher proportion of individuals with the highest predicted uplift are being targeted.

Qini Coefficient:

The Qini coefficient works on the same idea as the Cumulative Gain, with one key distinction.

The Qini coefficient aims to penalize models that overestimate treatment samples and underestimate control samples.

The formula to calculate it:

Qini coefficient


That’s great but how we are going to choose between different models ? Relying solely on these curves to choose between different models might not be the most data-driven approach.

The quality metrics

There are three the most useful metrics that can help us and all of them are applicable to both Qini and Cumulative Gain approaches.

Area under Uplift (AUC-U):

Similar to the area under the ROC curve (AUC-ROC) in traditional classification, the AUC-U measures the overall performance of an uplift model. It calculates the area under the uplift / Qini curve, which represents the cumulative uplift along individuals sorted by uplift model predictions. AUC

Uplift@K:

Uplift@K focuses on identifying the top K% of the population with the highest predicted uplift. It measures the proportion of truly responsive individuals within this selected group. A higher uplift@K value indicates a better model at targeting the right individuals.

In the example below [email protected] for the first model is roughly 0.16 and for the second model is 0.19 , and the choice of the best model is obvious. Uplift@K

When this metric can help ?

When it comes to the real business cases, we often encounter constraints imposed by our stakeholders. Sometimes ,we can only send notifications or campaigns to limited number of people , for example only 10k people.This is precisely when the Uplift@K metric comes to our rescue, making it the obvious choice.

Uplift max:

Uplift max refers to the maximum uplift achieved by the model. It represents the difference between the treated and control groups with the highest uplift scores. Uplift Max

Conclusion

We have witnessed that traditional classification and regression metrics may not adequately measure uplift models’ effectiveness.

To overcome this, two primary approaches, CG and Qini, offer valuable metrics for evaluation.

However, the choice of metrics ultimately depends on your specific business goals.

It is crucial to continuously experiment with different variations and find the metrics that align best with your objectives. By exploring and refining your approach, you can effectively measure the impact of uplift models and optimize their performance.

\n

\

Calculating a Dynamic Truncated Mean in Power BI Using DAX: A Quick Guide

2026-01-11 04:00:04

Why You Need a Truncated Mean

In data analysis, the standard AVERAGE function is a workhorse, but it has a significant weakness: it is highly susceptible to distortion from outliers. A single extreme value, whether high or low, can skew the entire result, misrepresenting the data's true central tendency.

\ This is where the truncated mean becomes essential. It provides a more robust measure of the average by excluding a specified percentage of the smallest and largest values from the calculation.

\ While modern Power BI models have a built-in TRIMMEAN function, this function is often unavailable when using a Live Connection to an older Analysis Services (SSAS) model. This article provides a robust, manual DAX pattern that replicates this functionality and remains fully dynamic, responding to all slicers and filters in your report.

The DAX Solution for a Dynamic Truncated Mean

This measure calculates a 20% truncated mean by removing the bottom 10% and top 10% of values before averaging the remaining 80%.

\ You can paste this code directly into the "New Measure" formula bar.

Trimmed Mean (20%) =
VAR TargetTable = 'FactTable'
VAR TargetColumn = 'FactTable'[MeasureColumn]
VAR LowerPercentile = 0.10 // Defines the bottom 10% to trim
VAR UpperPercentile = 0.90 // Defines the top 10% to trim (1.0 - 0.10)

// 1. Find the value at the 10th percentile
VAR MinThreshold =
    PERCENTILEX.INC(
        FILTER(
            TargetTable,
            NOT( ISBLANK( TargetColumn ) )
        ),
        TargetColumn,
        LowerPercentile
    )

// 2. Find the value at the 90th percentile
VAR MaxThreshold =
    PERCENTILEX.INC(
        FILTER(
            TargetTable,
            NOT( ISBLANK( TargetColumn ) )
        ),
        TargetColumn,
        UpperPercentile
    )

// 3. Calculate the average, including only values between the thresholds
RETURN
CALCULATE(
    AVERAGEX(
        FILTER(
            TargetTable,
            TargetColumn >= MinThreshold &&
            TargetColumn <= MaxThreshold
        ),
        TargetColumn
    )
)

Deconstructing the DAX Logic

This formula works in three distinct steps, all of which execute within the current filter context (e.g., whatever slicers the user has selected).

  1. Define Key Variables
  • TargetTable & TargetColumn: We assign the table and column names to variables for clean, reusable code. You must change 'FactTable'[MeasureColumn] to match your data model.
  • LowerPercentile / UpperPercentile: We define the boundaries. 0.10 and 0.90 mean we are trimming the bottom 10% and top 10%. To trim 5% from each end (a 10% total trim), you would use 0.05 and 0.95.

2. Find the Percentile Thresholds

  • MinThreshold & MaxThreshold: These variables store the actual values that correspond to our percentile boundaries.
  • PERCENTILEX.INC: We use this "iterator" function because it allows us to first FILTER the table.
  • `FILTER(…, NOT(ISBLANK(…))): This is a crucial step. We calculate the percentiles only for rows where our target column is not blank. This prevents BLANK() values from skewing the percentile calculation.
  • The result is that MinThreshold holds the value of the 10th percentile (e.g., 4.5) and MaxThreshold holds the value of the 90th percentile (e.g., 88.2) for the currently visible data.

3. Calculate the Final Average

  • RETURN CALCULATE(...): The CALCULATE function is the key to making the measure dynamic. It ensures the entire calculation respects the filters applied by any slicers or visuals in the report.
  • AVERAGEX(FILTER(...)): The core of the calculation. We use AVERAGEX to iterate over a table.
  • FILTER(...): We filter our TargetTable a final time. This filter is the "trim." It keeps only the rows where the value in TargetColumn is:
  • Greater than or equal to our MinThreshold
  • AND
  • Less than or equal to our MaxThreshold
  • AVERAGEX(..., TargetColumn): AVERAGEX then calculates the simple average of TargetColumn for only the rows that passed the filter.

Conclusion

By implementing this DAX pattern, you create a robust, dynamic, and outlier-resistant KPI. This measure provides a more accurate picture of your data's central tendency and will correctly re-calculate on the fly as users interact with your Power BI report.


Thank you for taking the time to explore data-related insights with me. I appreciate your engagement. If you find this information helpful, I invite you to follow me or connect with me on LinkedIn or X(@Luca_DataTeam). Happy exploring!👋

I Built an Enterprise-Scale App With AI. Here’s What It Got Right—and Wrong

2026-01-11 03:00:25

To get a first hand understanding of the impact of AI on the software development lifecycle (SDLC), I decided to run an experiment. I wanted to try and write a reasonably complex system from scratch using AI. I didn’t want a "Hello World" or another “To Do” app; I wanted something realistic, something that could be used at scale like we'd build in the enterprise world.

The result is Under The Hedge—a fun project blending my passion for technology and wildlife.

The experiment yielded several key findings, validating and adding practical context to the broader industry trends:

  • Is AI making developers faster or just worse? I ran an experiment to find out.
  • The "Stability Tax": Discover the hidden cost of high-speed AI code generation and why it's fueling technical debt.
  • Vibe Coding is Dead: Learn why generating code via natural language prompts is raising the bar for developer mastery, not lowering it.
  • The Trust Paradox: Why 90% of developers use AI, but 30% don't trust a line of code it writes.
  • The Bricklayer vs. The Site Foreman: A new model for the developer's role in the age of AI.

The Project: Under The Hedge

I set out to build a community platform for sharing and discovering wildlife encounters—essentially an Instagram/Strava for wildlife.

Under the Hedge: Connect Through Wildlife

To give you a sense of the project's scale, it includes:

  • AI-Powered Analysis: Users upload photos, and the system uses Gemini to automatically identify species, describe behavior, and assign an "interest score" based on awareness of what’s going on in the image and the location it was taken.
  • Complex Geospatial Data: Interactive maps, geohashing for location following, and precise coordinate extraction from EXIF data.
  • High-Performance Data Layer: A scalable and bleeding-fast single-table design in AWS DynamoDB to handle complex data access patterns with sub-millisecond latency.
  • Scalable Media Infrastructure: A robust media component using AWS CloudFront to efficiently cache and serve high-resolution images and videos to users globally.
  • Social Graph: A full following system (follow Users, Species, Locations, or Hashtags), threaded comments, and activity feeds.
  • Gamification: Place leaderboards to engage locals.
  • Enterprise Security: Secure auth via AWS Cognito, privacy controls, and moderation tools.

You can check it out here: https://www.underthehedge.com

The Industry Context

Before I share what I found while developing Under The Hedge, we should assess what the rest of the industry is saying based on the studies from the last couple of years.

As we come to the end of 2025, the narrative surrounding AI-assisted development has evolved from simple "speed" to a more nuanced reality. The 2025 DORA (DevOps Research and Assessment) report defines this era with a single powerful concept: AI is an amplifier. It does not automatically fix broken processes; rather, it magnifies the existing strengths of high-performing teams and the dysfunctions of struggling ones.

Throughput vs. Stability

The 2025 data reveals a critical shift from previous years. In 2024, early data suggested AI might actually slow down delivery. However, the 2025 DORA report confirms that teams have adapted: AI adoption is now positively correlated with increased delivery throughput. We are finally shipping faster.

But this speed comes with a "Stability Tax." The report confirms that as AI adoption increases, delivery stability continues to decline. The friction of code generation has been reduced to near-zero, creating a surge in code volume that is overwhelming downstream testing and review processes.

Vibe Coding Bug Spike

This instability is corroborated by external studies. Research by Uplevel in 2024 found that while developers feel more productive, the bug rate spiked by 41% in AI-assisted pull requests. This aligns with the "vibe coding" phenomenon—generating code via natural language prompts without a deep understanding of the underlying syntax. The code looks right, but often contains subtle logic errors that pass initial review.

The Trust Paradox

Despite 90% of developers now using AI tools, a significant "Trust Paradox" remains. The 2025 DORA report highlights that 30% of professionals still have little to no trust in the code AI generates.

We are using the tools, but we are wary of them—treating the AI like a "junior intern" that requires constant supervision.

Code Churn and Technical Debt

The Death of "DRY" (Don't Repeat Yourself) The most damning evidence regarding code quality comes from GitClear’s 2025 AI Copilot Code Quality report. Analyzing 211 million lines of code, they identified a "dubious milestone" in 2024: for the first time on record, the volume of "Copy/Pasted" lines (12.3%) exceeded "Moved" or refactored lines (9.5%).

The report details an 8-fold increase in duplicated code blocks and a sharp rise in "churn", code that is written and then revised or deleted within two weeks. This indicates that AI is fueling a "write-only" culture where developers find it easier to generate new, repetitive blocks of code rather than refactoring existing logic to be modular. We are building faster, but we are building "bloated" codebases that will be significantly harder to maintain in the long run.

Security Risks

Finally, security remains a major hurdle. Veracode’s 2025 analysis found that 45% of AI-generated code samples contained insecure vulnerabilities, with languages like Java seeing security pass rates as low as 29%.

So what do these studies tell us?

The data paints a clear picture: AI acts as a multiplier. It amplifies velocity, but if not managed correctly, it also amplifies bugs, technical debt, and security flaws.

What my Experiment Taught Me

My chosen tools were Gemini for architecture/planning and Cursor for implementation. In Cursor I used agent mode with the model set to auto.

Building Under The Hedge was an eye-opening exercise that both confirmed the industry findings and highlighted the practical, human element of AI-assisted development.

The Velocity Multiplier

While I didn't keep strict time logs, I estimate I could implement this entire system—a reasonably complex, enterprise-scale platform—in less than a month of full-time work (roughly 9-5, 5 days a week). This throughput aligns perfectly with the DORA report's finding that AI adoption is positively correlated with increased delivery throughput.

The greatest personal impact for me, which speaks perhaps more about motivation than pure speed, was the constant feedback loop. In past personal projects, I often got bogged down in small, intricate details, leading to burnout. Using these tools, I could implement complete, complex functionality—such as an entire social feed system—in the time it took to run my son’s bath. The rapid progress and immediate results are powerful endorphin hits, keeping motivation high.

The "Stability Tax" in Practice

My experience also validated the industry's growing concerns about the "Stability Tax"—the decline in delivery stability due to increased code volume. I found that AI does well-defined, isolated tasks exceptionally well; building complex map components or sophisticated media UIs was done in seconds, tasks that would typically take me days or even weeks. However, this speed often came at the expense of quality:

  • Bloat and Duplication: The AI consistently defaulted to the fastest solution, not the best one, unless explicitly instructed otherwise. This led to inefficient, bloated code. When tackling a difficult issue, it would often "brute force" a solution, implementing multiple redundant code paths in the hope of fixing the problem.
  • The Death of "DRY" Confirmed: I frequently observed the AI duplicating whole sections of code instead of creating reusable components or helper methods. This is direct evidence of the "write-only" culture highlighted in the GitClear report, fueling the rise in copied/pasted lines and code churn. If I changed a simple data contract (e.g., renaming a database property), the AI would often try to maintain backwards compatibility by handling both the old and new scenarios, leading to unnecessary code bloat.

Ultimately, I had to maintain a deep understanding of the systems to ensure best practices were implemented, confirming the "Trust Paradox" where developers treat the AI like a junior intern requiring constant supervision.

Security and Knowledge Gaps

The security risks highlighted by Veracode were also apparent. The AI rarely prioritized security by default; I had to specifically prompt it to consider and implement security improvements.

Furthermore, the AI is only as good as the data it has access to. When I attempted to integrate the very new Cognito Hosted UI, the model struggled significantly, getting stuck in repetitive loops due to a lack of current training data. This forced me to step back and learn the new implementation details myself. Once I understood how the components were supposed to fit together, I could guide the AI to the correct solution quickly, highlighting that a deep conceptual understanding is still paramount.

AI as a "Coaching Tool"

Despite its flaws, AI proved to be a magnificent tool for learning. As a newcomer to Next.js and AWS Amplify, the ability to get working prototypes quickly kept me motivated. When I encountered functionality I didn't understand, I used the AI as a coach, asking it to explain the concepts. I then cross-referenced the generated code with official documentation to ensure adherence to best practices. By actively seeking to understand and then guiding the AI towards better solutions, I was able to accelerate my learning significantly.

How to Help AI Be a Better Code Companion

To mitigate the "Stability Tax" and maximize the AI's velocity, a proactive, disciplined approach is essential:

  1. Detailed Pre-Planning is Key: Use tools like Gemini (leveraging its deep research feature) to create detailed specifications, architecture diagrams, and design documents before starting implementation. This "specification first" approach provides the AI with a clearer target, leading to more predictable and robust output.
  2. Explicitly Enforce Quality Gates: Instead of relying on the AI to spontaneously generate quality code, we must proactively instruct it to maintain standards. This includes designing regular, specific prompts focused on:
  • Identifying security improvements.
  • Identifying performance issues or potential optimisations.
  • Identifying duplicated or redundant code.
  1. Leverage AI for Quality Assurance: Use the AI to retrospectively analyze generated code and identify areas for refactoring or improvement, a task it can perform far faster than a manual human review.
  2. Use AI for the Entire SDLC: We should deploy AI to write and self-assess feature design documents, epics, and individual tasks, and crucially, to write comprehensive test plans and automated tests to catch the subtle logic errors associated with "vibe coding."

Conclusion: The End of "Vibe Coding"

So, should we stop using AI for software development?

Absolutely not. To retreat from AI now would be to ignore the greatest leverage point for engineering productivity we have seen in decades. Building Under The Hedge proved to me that a single developer, armed with these tools, can punch well above their weight class, delivering enterprise-grade architecture in a fraction of the time.

However, the era of blind optimism must end. The "Sugar Rush" of easy code generation is over, and the "Stability Tax" is coming due.

The data and my own experience converge on a single, inescapable truth: AI lowers the barrier to entry, but it raises the bar for mastery.

Because AI defaults to bloating codebases and introducing subtle insecurities, the human developer is more critical than ever. Paradoxically, as the AI handles more of the syntax, our value shifts entirely to semantics, architecture, and quality control. We are transitioning from being bricklayers to being site foremen.

If we treat AI as a magic wand that absolves us of needing to understand the underlying technology, we will drown in a sea of technical debt, "dubious" copy-paste patterns, and security vulnerabilities. But, if we treat AI as a tireless, brilliant, yet occasionally reckless junior intern—one that requires strict specifications, constant code review, and architectural guidance—we can achieve incredible things.

The path forward isn't to stop using the tools. It is to stop "vibe coding" and start engineering again. We must use AI not just to write code, but to challenge it, test it, and refine it.

The future belongs to those who can tame the velocity. I only wish my experiment resulted in building something that would make me loads of money instead of just tracking pigeons! 😂

Thank you for reading, please check out my other thoughts at denoise.digital.