Category Archives: Articles in English

My AI experiments

These are just a few notes about my experiments with AI coding tools.

Key takeaways

  1. AI tools will indeed change the way we code. It’s a useful tool for some tasks, a distraction for others.
  2. To me, they are especially helpful for chores I don’t want to do. (learning and forgetting yet another YAML configuration format, writing boilerplate code, bash scripts etc.)
  3. AI is far from replacing engineers. It often gets stuck on trivial issues and then attempts random changes to fix the problem.
  4. AI usage leads to a lot of generated code. Without AI, my laziness pushes me to code reuse, abstraction and better design. AI can generate hundreds lines of code in a minute. My laziness pushes me to accept all the code, which can lead to a worse design.

All of the examples below are related to ShedLock, an OS project I am maintaining.

New Firestore provider

ShedLock can integrate with various DB technologies and from time to time I get a request to support a new exotic one. Recently I got a request to support Firestore. Usually I ask the requestor to send a PR. This time, I asked Junie to implement it. The process is pretty streamlined, the API is well defined, the structure of the code is similar between technologies, there is a test-suite that needs to pass, so this is an ideal task for AI.

Junie checked the structure of the project, implemented the code. Then I asked it to implement tests. Again, it found which class to extend to get the test suite, configured Firestore in testcontainers and almost did it all. But then it got stuck on a simple problem, it was configuring the test client using setHost("http://" + firestoreEmulator.getEmulatorEndpoint()). It’s kinda obvious that the host should not have the "http://" prefix, but not to the AI. It started to configure random environment variables, and do other random changes without solving the problem. But other than that, AI has been a huge time-saver. The implementation took like 15 minutes, you can see it here.

Fixing BOM

ShedLock provides BOM with a list of all modules. So every time, somebody adds a module, they should add it to the BOM. And surprise, surprise, people often forget. I wanted to check if there are some modules missing, but checking it manually is boring. You need to compare the list of modules with the BOM. Or you can ask your friendly AI and you are done.
Again, a huge time-saver. But what if you want to check the completeness in the build-time for each PR?

Checking BOM completeness

Let’s ask Claude “Is there a way (Maven plugin or something similar) to check that all modules are mentioned in BOM”. Claude goes directly for a bash script, trying to use xmllint. Which is not installed on my machine, I then try to steer it to use Java or Groovy, but at the end, it tried few approaches and it came up with a bash script like this . Does it work? Yes. Do I like it? No. Do I know a better way? No. And this is the tricky part. I have a working solution, which I don’t like. Maybe there is a better one. Maybe one of the solutions the model tried was the right one and did not work due to some trivial issue. I can continue prodding the AI to give me a better solution, and spend the whole day doing that.

For example, I can ask it to use Maven, it will praise my cleverness, and will spit out a Maven enforcer rule like this. Do I like it? Almost.

This is interesting. Without AI, I would have checked more thoroughly if there is no better way. If not, I would have abandoned the task as the value is not worth the effort. Or, I would have implemented the plugin, and shared it with others. But with AI, the effort to publish it is way bigger than the almost free implementation. So I will keep the plugin for myself. I am afraid we will see this more and more often. Instead of reusing tools and simple apps, we will generate tailor made ones. With AI it’s faster and easier to generate a simple tool than to search and evaluate existing ones.

Debugging flaky test

I had a flaky test. Time to time I got a few millisecond difference in one of the tests.

Error:    MsSqlExposedLockProviderIntegrationTest>AbstractJdbcLockProviderIntegrationTest.shouldCreateLockIfRecordAlreadyExists:81->AbstractLockProviderIntegrationTest.shouldCreateLock:49->AbstractJdbcLockProviderIntegrationTest.assertUnlocked:56 [is unlocked] 
Expecting actual:
  2025-06-19T16:01:23.840Z
to be before or equal to:
  2025-06-19T16:01:23.839Z

Here the AI failed completely. Event though, the issue was caused by a subtle bug in the code (the fix is here) Most of the models wanted to fix the test by add a buffer. And if I pointed that I suspect a bug in the code, this is what I got

Now I see the issue! The problem is that the Exposed provider is using LocalDateTime (datetime columns and CurrentDateTime) but the test is expecting timezone-aware timestamps.

Timezone issue causing a millisecond difference? Come on.

To be fair, the AI provided other plausible possible reasons for the issue, but it never guessed the right one. One of the models actually pointed to the line with the bug, but praised it for correctly handling MsSQL time rounding issues.

So to summarize, AI is a useful tool, if you are not already playing with it, pick a chore you are procrastinating on, and give it a try.

Mistakes I made designing JsonUnit fluent API

Several years ago, I have decided that it would be nice to add fluent API to JsonUnit library. If you are not familiar with fluent assertion APIs it looks like this.

assertThatJson("{\"test\":1}").node("test").isEqualTo(1);

assertThatJson("[1 ,2]").when(IGNORING_ARRAY_ORDER).isEqualTo("[2, 1]");

I really like fluent assertions. First of all, they are really easy to use, you get help from your IDE unlike when using Hamcrest static methods. Moreover, they are quite easy to implement. This is how the original implementation looked like.

public class JsonFluentAssert {

    protected JsonFluentAssert(...) {
    }

    public static JsonFluentAssert assertThatJson(Object json) {
        return new JsonFluentAssert(...);
    }

    public JsonFluentAssert isEqualTo(Object expected) {
        ... 
    }

    public JsonFluentAssert node(String path) {
        ...
    }

    public JsonFluentAssert when(Option firstOption, Option... otherOptions) {
    ...
    }

    ...
}

We have static factory method assertThatJson() that creates JsonFluentAssert. And all other methods return the same class so you can chain them together. Nice and simple. Unfortunately, there are three mistakes in this API. Do you see them? Congratulations if you do. If not, do not be sad, it took me several year to see them

Unnecessarily complicated

The biggest mistake is that the API supports chaining even after the assertion method isEqualTo() is called. It seems I designed this way on purpose since there is a test like this

assertThatJson("{\"test1\":2, \"test2\":1}")
    .node("test1").isEqualTo(2)
    .node("test2").isEqualTo(1);

The problem is that to support such rare use-case, now the API is more error prone. I got several error reports complaining that this does not work

assertThatJson("[1 ,2]").isEqualTo("[2, 1]").when(IGNORING_ARRAY_ORDER);

It looks reasonable, but it can not work. The comparison has to be done in isEqualTo and if it fails, it throws an assertion error so when() method is not called at all. In current design you can not postpone the comparison since we do not know which method is the last one in the invocation chain. OK, how to fix it? Ideally, isEqualTo() should have returned void or maybe some simple type. But I can not change the contract, it would break the API.

If I can not change the API, I can do the second best thing – mark it as deprecated. This way the user would at least get a compile time warning. It seems simple, I just need to return a new type from isEqualTo() with when() method marked as deprecated.

Methods return class not an interface

Here comes second mistake – isEqualTo() and other methods return a class not an interface. An interface would have given me more space to maneuver. And again, I can not introduce an interface without potentially breaking backwards compatibility. Some clients might have stored the expression result in an variable like this

JsonFluentAssert node1Assert = 
assertThatJson("{\"test1\":2, \"test2\":1}").node("test1");

If I want to mark method when() as deprecated if called after assertion, isEqualTo() has to return a subclass of JsonFluentAssert like this

public class JsonFluentAssert {

    private JsonFluentAssert(...) {
    }

    public static JsonFluentAssert assertThatJson(Object json) {
        return new JsonFluentAssert(...);
    }

    public JsonFluentAssertAfterAssertion isEqualTo(Object expected) {
        ... 
    }

    public JsonFluentAssert node(String path) {
        ...
    }

    public JsonFluentAssert when(Option firstOption, Option... otherOptions) {
        ...
    }
    ...
    public static class JsonFluentAssertAfterAssertion extends JsonFluentAssert {

        @Override
        @Deprecated
        public JsonFluentAssert when(Option firstOption, Option... otherOptions) {
            return super.when(firstOption, otherOptions);
        }
   }
}

Extensibility

Third mistake was to make the class extensible by making the constructor protected and not private. I doubt anyone actually extends the class but I can not know for sure. And I can not change signature of
isEqualTo() without breaking subclasses.

Solution

It’s pretty tricky situation. I can either keep broken API or break backward compatibility, tough choice. At the end I have decided to bet on the fact that no one is extending JsonFluentAssert and I did the change depicted above. I may be wrong but there is nothing else I could do, I do not want to live with bad API design forever. But if you have a better solution, please let me know. Full source code is available here.

Machine Learning – word2vec results

Last time we have discussed word2vec algorithm, today I’d like to show you some of the results.

They are fascinating. Please remember that word2vec is an unsupervised algorithm, you just feed it a lot of text and it learns itself. You do not have to tell it anything about the language, grammar, rules, it just learns by reading.

What’s more, people from Google have published a model that is already trained on Google news, so you can just download the model, load it to you Python interpreter and play. The model has about 3.4G and contains 3M words, each of them represented as a 300-dimensional vector. Here is the source I have used for my experiments.

from gensim.models import Word2Vec
model = Word2Vec.load_word2vec_format('GoogleNews-vectors-negative300.bin', binary=True)
# take father, subtract man and add woman
model.most_similar(positive=['father', 'woman'], negative=['man'])

[('mother', 0.8462507128715515),
 ('daughter', 0.7899606227874756),
 ('husband', 0.7560455799102783),
 ('son', 0.7279756665229797),
 ('eldest_daughter', 0.7120418548583984),
 ('niece', 0.7096832990646362),
 ('aunt', 0.6960804462432861),
 ('grandmother', 0.6897341012954712),
 ('sister', 0.6895190477371216),
 ('daughters', 0.6731119751930237)]

You see, you can take vector for “father” subtract “man” and add “woman” and you will get “mother”. Cool. How does it work? As we have discussed the last time, word2vec groups similar words together and luckily it also somehow discovers relations between the words. While it’s hard to visualize the relations in 300-dimensional space, we can project the vectors to 2D.

plot("man woman father mother daughter son grandmother grandfather".split())
Family relationships

Now you see how it works, if you want to move from “father“ to “mother“, you just move down by the distance which is between “man” and “woman”. You can see that the model is not perfect. One would expect the distance between “mother” and “daughter” will be the same as between “father” and “son”. Here it is much shorter. Actually, maybe it corresponds to reality.

Let’s try something more complicated

plot("pizza hamburger car motorcycle lamb lamp cat cow sushi horse pork pig".split())
Food

In this example, we loose lot of information by projecting it to 2D, but we can see the structure anyway. We have food on the left, meat in the middle, animals on the right and inanimate objects at the top. Lamb and lamp sound similar but it did not confuse the model. It just is not sure if a lamb it’s meat or an animal.

And now on something completely different – names.

plot("Joseph David Michael Jane Olivia Emma Ava Bill Alex Chris Max Theo".split())
Names

Guys on the left, gals on the right and unisex in the middle. I wonder, though what the vertical axis means.

I have played with the model for hours and it does not cease to surprise me. It just reads the text and learns so much, incredible. If you do not want to play with my code you can also try it online.