Sunday, 18 October 2026

The Fall of Nestor: A Greek Tragedy in the Age of AI

An entirely personal and somewhat uncomfortable opinion about software engineering in 2026.

Today I was looking into the cost of AI-powered code reviews with Claude. Around $15–25 per substantial review. That's quite a price, especially in Europe, where engineering salaries are not exactly Silicon Valley material.

And what are we buying?

Mostly technical analysis, detached from the real-world context. AI can find bugs, identify edge cases, and occasionally discover something genuinely important. It can also produce a beautifully articulated hypothetical problem that requires another AI session to translate from Claudish into English.

Sometimes the complexity of explaining a finding is itself a good indication that nobody will ever encounter it.

But here's the question: Who remains the last guardian of every important merge request?

Still humans.

Someone must understand the business context, accept responsibility, make a decision, and eventually own whatever breaks in production. Even with excellent AI tools, organizing a proper review process remains surprisingly difficult.

And this brings us to our hero.

Enter Nestor, King of Pylos

In Homer's Iliad, Nestor is the oldest and wisest of the Greek warrior-kings. A respected veteran, always ready to advise younger warriors, usually after a lengthy introduction explaining how much better everything was in his youth.

Every software organization has its own Nestors.

Ten to twenty years of experience. Knows the sacred codebase. Remembers why that strange workaround was introduced in 2014. Can explain why the team should never use that particular database again.

His working day might include reviewing zero to two merge requests, attending a few meetings, and progressing on some architectural or technical-debt initiative with a timeline measured in weeks, certainly not hours.

And for a long time, this was a perfectly reasonable arrangement.

Software engineering was a profession with an interesting economic balance. Take the Dutch market, for example: an average software engineer earns around 53% of a doctor's salary, 138% of an electrical engineer's, and 176% of a teacher's.

Yet unlike doctors, engineers rarely carried personal responsibility for someone's life. Unlike teachers, they had relatively little direct exposure to the people consuming their services.

A fairly comfortable working environment, good compensation, and sometimes an employer still trying to convince everyone to visit the office by providing free coffee.

Not a bad deal for the defenders of Olympus.

The first cracks in the temple

Somewhere between 2018 and 2024, Nestor began losing one of his most important sources of job security: exclusive knowledge.

Documentation as code became fashionable. Internal developer platforms became more accessible. Backstage, Argo CD, infrastructure as code, and standardised delivery pipelines gradually turned sacred operational rituals into something ordinary engineers could understand and operate.

Knowledge started moving from people's heads into systems.

But Nestor survived.

After all, becoming an experienced engineer still required years of education, five to ten years of professional experience, and at least a year or two of understanding a particular business domain.

Even with excellent documentation, expertise wasn't free.

You still had to learn, remember, practice, and occasionally discover that the technology you spent three years mastering was now deprecated.

Meanwhile, interviews demanded LeetCode exercises, system-design performances, and other rituals seemingly invented by Greek gods to punish mortals for choosing computer science.

The profession was difficult enough to justify its privileges.

Or so we told ourselves.

The comfortable years

Of course, there were outstanding engineers, people whose experience saved organisations millions and prevented disasters nobody even knew were possible.

But there were others.

Veterans who had gradually lost interest in the business, developed near-zero tolerance for their internal customers, and learned to hide questionable productivity behind architecture discussions, technical debt, and mysterious engineering concerns.

Every organization has a few demons that nobody can defeat.

Technical debt is particularly convenient because it is both absolutely real and infinitely expandable.

No product owner can easily challenge an engineer who announces that the architecture is becoming unsustainable.

And so our heroes continued guarding the gates.

Sometimes they were protecting the organisation.

Sometimes they were protecting their comfort.

Usually a little of both.

Then Prometheus brought another fire

And here we are in 2026.

Frontier AI models can generate merge requests against previously sacred codebases, analyse legacy architectures, explain unfamiliar dependencies, and propose changes in minutes.

Things that once required a senior engineer spending several days reading code are increasingly accessible to anyone who knows how to ask good questions and verify the answers.

The tools aren't perfect. Far from it.

But perfection was never the requirement for disrupting a profession.

Being useful enough, cheap enough, and improving faster than the established workflow is already dangerous.

An energetic junior or mid-level developer using AI effectively can sometimes deliver ten times the visible output of a veteran who refuses to change.

Of course, ten times more code isn't ten times more value. Sometimes it's ten times more trouble.

But the productivity difference is becoming increasingly difficult to ignore.

And Nestor?

Nestor opens the merge request, examines the generated code, and offers his wisdom:

"You could use Integer here instead of Long."

Thank you, mighty king of Pylos.

We have spent months tuning our AI review tools to produce fewer low-value comments. And now you are doing it manually.

The oracle has entered the meeting

Naturally, Nestor eventually starts using AI too.

Not necessarily because he believes in it, but because using AI tools has become mandatory for engineers who want to remain competitive.

Yet something fundamental has changed.

Previously, the veteran was the source of technical knowledge. Junior engineers approached him for answers, architectural explanations, and guidance through the labyrinth.

Today, a well-prepared session with a frontier model, connected to the codebase, documentation, and relevant MCP servers, can sometimes provide better technical consultancy than an experienced engineer.

It doesn't get tired.

It doesn't complain that the question was already answered in a meeting in 2021.

And it doesn't begin with a fifteen-minute lecture about why microservices were a mistake.

The veteran is no longer automatically the oracle.

Increasingly, he is an interpreter, validator, and decision-maker standing between the oracle and reality.

That is an important job.

But it's a different job.

And perhaps the most uncomfortable realisation is that the value of knowing something is falling faster than the value of understanding what matters.

The last guardians

The good news is that the promised lights-out software factories remain largely a fantasy. Generating software and operating a reliable business are very different things.

Someone must still understand customers, negotiate requirements, manage risks, make trade-offs, and take responsibility when production catches fire at 3 AM.

The bad news is that technical expertise alone offers less job security than it once did. Business understanding, communication, judgement, and accountability are becoming more valuable relative to the ability to write sophisticated code.

What we used to call soft skills may turn out to be some of the hardest things to automate.

Nestor's old authority came from knowing things others didn't. His new authority must come from understanding what matters, making decisions, and taking responsibility for their consequences.

The gods have changed the rules

I don't believe software engineering is disappearing. But some of the privileges and professional habits we became accustomed to are being challenged, perhaps not entirely without reason.

For years, technical complexity translated into high compensation, autonomy, and job security. Sometimes the value delivered justified all three. Sometimes complexity itself became the justification.

Now Nestor must change.

Can the industry motivate that change with higher salaries? Perhaps. But looking at the current European market dynamics, the trend hardly seems encouraging.

Is high cognitive load still an asset worth managing, rather than something to avoid through isolation, endless procrastination, and technical distractions? Most probably yes.

The profession may increasingly reward those willing to carry that cognitive load: understanding messy business realities, making uncomfortable decisions, and accepting responsibility rather than hiding behind technical expertise.

The future is far less obvious than it seemed two years ago.

Nestor may remain the wisest warrior in the Greek army. But wisdom is no longer demonstrated by remembering how battles were fought twenty years ago.

It is demonstrated by helping win the battle happening today.

And somewhere on Olympus, the gods have already opened another pull request.

No reviewers assigned.

Saturday, 19 September 2026

[3 of 3] I benchmarked five Scala libraries. Which code would I rather read?

I started with synthetic benchmarks in part 1. Adding socket I/O in part 2 sent me into Kyo's scheduler. Along the way, Ox and Gears caught my attention. Their TCP throughput was close to cats-effect, they allocated much less, and I found their direct-style code easier to read.

For this last post, I added plain Loom and checked whether lower allocation also meant less memory. The results still don't give me one choice that wins everywhere.

Five runtimes, different answers

These are selected results from the new run, in operations per second. Higher is better. The ± values are JMH's 99.9% confidence-interval half-widths; compare libraries within each column.

Throughput, including runtime entry and task management
Runtime8 workersSpawn/joinBlocking TCPCallback TCP
cats-effect3,060 ± 921,894 ± 85145.10 ± 9.03141.53 ± 3.07
Kyo1,399 ± 265,355 ± 1044.95 ± 0.03139.99 ± 1.73
Loom / JDK3,420 ± 71558 ± 43143.49 ± 5.65137.69 ± 3.44
Ox3,580 ± 230494 ± 51140.29 ± 6.62136.78 ± 3.48
Gears3,214 ± 50377 ± 26140.28 ± 9.81139.25 ± 2.57

The eight-worker case processes 4,096 values, with 64 arithmetic rounds per value. Spawn/join creates a child, waits for it to finish, and repeats 1,000 times. Each TCP operation completes 256 exchanges over 64 persistent loopback connections. A separate server requests a 1 ms delay per response.

Kyo leads the tiny-task spawn/join case, while the callback TCP intervals overlap across all five. The slow Kyo blocking result uses defaults. In part 2, flushing and worker tuning brought it close to CE. That tuned result belongs to the earlier experiment.

All five ran every case shown here. The Loom row uses JDK virtual threads and primitives. The suspended effect-chain tests from part 1 have no equivalent Loom, Ox or Gears benchmark: replacing the chain with a direct loop changes what we measure. I haven't compared cancellation or resource-safety guarantees.

Less allocation didn't answer the memory question

I kept 100,000 tasks waiting, each holding a 1 KiB payload, and measured live heap after full GC and macOS process footprint. I also sampled memory during blocking TCP work at 64 connections. The numbers are medians from three fresh JVMs per case, in MiB. TCP measurements cover only the client.

Memory consumption: lower is better
RuntimeWaiting tasks: live heapWaiting tasks: process footprintActive TCP: process footprint
cats-effect191.0418.5152.5
KyoNot measured
Loom / JDK228.7390.090.3
OxNot measured
GearsNot measured

CE used less live heap for these waiting tasks. Loom had the smaller process footprint, which includes memory beyond live heap objects. So far, the memory harness covers only CE and Loom. I haven't measured Kyo, Ox or Gears here; the blanks don't indicate missing library support.

Both studies ran on 19 September 2026 on one M3 Max with JDK 25.0.3 and Scala 3.8.4: CE 3.7.1, Kyo 1.0.0-RC6, Ox 1.0.6 and Gears 0.3.1. Throughput used three forks, three 1-second warmups and three 1-second measurements, with a fixed 2 GiB G1 heap. Memory used G1 with a 64 MiB initial and 2 GiB maximum heap. These are short runs of these particular implementations on one machine.

What I want to maintain

I still enjoy writing with cats-effect and Kyo. After moving into Tagless Final, though, I sometimes find myself reading through a lot of ceremony to get to the business logic. Making every service generic in F[_] is a choice I can reconsider while still using CE.

That makes me more interested in direct-style concurrency. I often find its control flow easier to follow, even though writing effects is more fun for me. The benchmark results give me reasons to consider both.

AI makes me reconsider what I value

AI tools handled most of my earlier effect-system migration. If I spend more time reviewing generated code and less time writing it, how much should my enjoyment of writing a particular style count?

That even puts Java back on the table for me. Boilerplate bothers me less when I'm not typing all of it, but I still have to read and maintain it. I could live with more verbose code if I found it easier to follow what runs, how it fails and who owns a resource.

Effects and types can help me review code too. A version with fewer combinators might still hide a cancellation or cleanup bug.

I don't have one library to recommend for every project. I'd choose around the workload, the guarantees I need and the integrations available. Among the options that fit, I'd give more weight to what my team can comfortably read and maintain, whoever wrote it.

Sunday, 13 September 2026

[2 of 3] Blocking I/O made Kyo 30x slower. Then I tried flush().

I spent a week giving Kyo a chance in a real service. AI coding tools handled most of the effect-system migration.

After the first synthetic comparison, I also wanted to see what happened when the programs had to wait for actual socket I/O. The numbers below come from a separate TCP benchmark.

I added Gears and Ox too. Same TCP exchanges, connection limits and ordered results. The nonblocking numbers were close. Blocking exposed a much bigger difference.

Kyo led cats-effect by about 12% for nonblocking I/O at concurrency 8. At 64, it trailed by about 3%. That is already too mixed for a blanket claim that Kyo is faster. The earlier synthetic wins still describe those particular operations.

For blocking I/O at concurrency 64, cats-effect completed about 149 batches per second. Default Kyo managed about 5.

Each batch contained 256 requests over 64 persistent connections. A separate server JVM requested a 1 ms sleep before each response. These measurements used cats-effect 3.7.1 and Kyo 1.0.0-RC6 on one Mac with JDK 25.0.3, recorded on 6 September 2026.

Blocking configurationBatches/s, higher is better
cats-effect default149.00 ± 2.93
Kyo default4.96 ± 0.02
Kyo with flush before every exchange81.86 ± 25.62
Kyo with flush and scheduler tuning144.89 ± 3.60

Three JVM forks per case. The ± values are JMH's 99.9% confidence-interval half-widths. Flush alone varied substantially; the tuned Kyo and default CE intervals overlap.

The source investigation pointed to queued children collecting on a few workers, slow adaptation to short blocking calls, and a placement scan that could miss idle workers.

Calling the scheduler's flush hook before each exchange redistributed queued work:

Sync.defer {
    kyo.scheduler.Scheduler.get.flush()
    io.blocking(lane, index)
}

That is the blocking branch from the benchmark. Flush made a huge difference, but reaching CE's range also required setting coreWorkers, minWorkers, maxWorkers and scheduleStride to 64. Those are experimental settings for this workload. No library code changed.

Flush does not move the blocking call to another thread or create more workers. It gives queued tasks another chance to run elsewhere. We called it before every exchange; we have not tested whether once per worker would be enough.

The manual step bothers me. Kyo supports blocking through Sync.defer, yet getting good performance here required knowing about a scheduler hook. CE handled the same workload well with IO.blocking and its default runtime.

Kyo's Finagle integration already calls flush inside its blocking hook. When that integration is enabled and the call goes through the hook, it performs the flush automatically. That does not cover every arbitrary blocking call, and it does not apply our worker tuning. We did not benchmark Finagle.

Putting flush in every Sync.defer would also affect cheap side effects. My reading is that this could add scheduling overhead; I have no maintainer confirmation of that design rationale. A dedicated blocking helper seems worth exploring.

Gears and Ox landed around 139 blocking batches/s at concurrency 64 and allocated much less than CE. Their direct style is interesting even without a throughput win: ordinary loops, conditions and local helpers around I/O, with fewer effect combinators. That may matter more in everyday code than a small benchmark lead.

Kyo still has my attention. I also want to know how much scheduler knowledge an application will need.

Full tables, allocation, CPU and raw measurements, plus methods and reproduction. The suite passed 438 correctness checks. This is one loopback workload; it does not establish production performance or equivalent cancellation guarantees.

Saturday, 5 September 2026

[1 of 3 ]Kyo vs cats-effect: promising numbers and one big surprise

Kyo vs cats-effect: promising numbers and one big surprise

I find Kyo one of the most interesting projects in Scala right now. I get the feeling that some of the complexity we accept might not be necessary.

That feeling is easy to get excited about. I wanted to see some numbers too. This is the first post in a series about Kyo and cats-effect, starting with a small synthetic war.

I compared cats-effect 3.7.1 with Kyo 1.0.0-RC6, using fs2 3.13.0 on the cats-effect side for streaming.

Getting a fair comparison took more work than I expected. Two methods can look equivalent while doing different things with batches, buffers, or failures.

The suite passed 984 correctness checks. I ran JMH with three JVM forks and identical heap settings on one Mac with JDK 25.

Both sides use the same worker strategy and stream batch boundaries. The success-collection row uses parallel attempts and filtering, rather than native Async.gather. The stream rows do not directly compare parEvalMap with mapPar.

These are selected results with very little work per element. Ratios compare mean throughput; CE means cats-effect.

ConstructionHigher throughput
Uncontended permit acquisition/releaseKyo ~5.02×
Queue-backed stream batchesKyo ~3.70×
Sequential fiber spawn/joinKyo ~2.75×
Parallel stream batchesKyo ~1.93×
Integer queue, one producer and consumerKyo ~1.45×
Bounded workersKyo ~1.13×
Parallel attempt and success collectionConfidence intervals overlap
Left-associated suspended binds, depth 10,000CE ~1,895×

The permit, queue, and fiber results make Kyo worth a closer look. Around 5× the throughput for uncontended permits and 3.7× for queue-backed stream batches are encouraging numbers, even for these small workloads.

A little CPU work changed the results too. Bounded workers went from a small Kyo lead to CE ahead by about 2.22×. Kyo’s lead in parallel stream batches shrank to about 1.11×.

Then there’s the last row. CE handled the 10,000-step left-associated bind chain at about 1,895× Kyo’s throughput. Kyo allocated about 1.6 GB per complete chain, versus roughly 943 KB for CE. Making the chain ten times longer increased Kyo’s allocation about a hundredfold. That points to quadratic scaling in this case, and it needs a closer look.

These results make me more interested in trying Kyo. There’s enough here to justify a small application experiment, and a specific weakness to investigate along the way.

This is one machine and a set of small workloads. Cancellation, resource safety, and real I/O still need their own testing.

Source, full results, confidence intervals, and reproduction commands.

If you spot an unfair comparison, please point me to the code. I’d like to get it right.

Thursday, 28 February 2019

Aux pattern in 5 minutes

Every-time using everytime shapeless.Generic that returns shapeless.Generic.Aux
I was wondering why the construction
type Aux[T, Repr0] = Generic[T] { type Repr = Repr0 }
is needed?

If you are impassioned and want to save a time: AUX pattern is wrapping abstract type member into generic parametrisation to solve compiler limitations.

While the Scala's abstract type members are preferred to generic parametrization sometimes there are the problems with making the code compilable.
Lets revisit Type level programming for Streams and start from the case when Aux pattern isn’t needed:

import cats.Show

implicit val requestFunc = new Function[Int, String] {
  override def apply(in: Int): String = s"hello $in"
}
def process[Req, Resp](request: Req)(implicit func: Function[Req, Resp], s: Show[Resp]): String 
  = func(request).show

Our process function just applies the func to request and then using Show type class to represent the result value. Show and func are implicitly injected according to Req and Resp types.

Lets rewrite using abstract type member:

trait Request[Req] {
  type Resp
  def response: Resp
}

implicit def string2Sting = new Request[Int] {
  type Resp = String
  override def response: String = "Hello"
}

def process2[T](value: T)(implicit request: Request[T], m: Show[request.Resp]): String = 
 request.response.show

Method process2 isn’t compilable because we are trying parametrise Show with request.Response type:
error: illegal dependent method type: parameter may only be referenced in a subsequent parameter section

To fix this we introduce proxy type (Aux pattern).

type Aux[T2, B2] = Request[T2] {type B = B2}

And changing the method to:

def process3[Req, Resp](request: Req)(implicit aux: Aux[Req, Resp], m: Show[Resp]): String = 
aux.response.show

As a result we create Aux type that wraps abstract type member into generic to overcome Scala's compiler limitation!

Tuesday, 19 February 2019

Switch career to machine learning specialist

There is continues hype around AI. As this hype stream is still far from been over and predictively is going to grow in time it's natural for the software engineers to pry what is happening in the area of AI Engineer Jobs.

A lot of us been career switchers - got non IT education math or engineering and end up in IT industry as software engineers. Exceptional professions like 3D engine developers etc always have been demanding to heavy math background.

What is about ML - there are many courses/books like learn it in N days. Career switchers are used to get an experience driven into new background with consistent feeling lick of knowledge.

Does this skill help with diving into ML? My opinion it isn't - before starting ML courses it's mandatory to learn/refresh match background - here is graph for studying ML from scratch:




Tuesday, 11 December 2018

Hylomorphism in 1 minute

A Hylomorphism is  Catamorphism compose to Anamorphism function.

It's presented as 

Hylomorphism = catamorphism(anamorphism(x))
 or
Hylomorphism = fold(unfold(x))

Basically it constructs (unfold) complex type (like trees, lists) and destructs (folds) back into representing value.

For example to get factorial from N we can 
a) unfold N to list of (n),(n-1),..,0
b) fold with prod function the list from previous step.

And possible Scala example implementing function for getting factorial from N with help of previously introduced list's Catamorphism and  Anamorphism is: