Spring Data and Spring Data Neo4j join the “No OFFSET Movement!”

Markus Winand has blogged about tool support for keyset pagination nearly a decade ago and of course my friend Lukas Eder has picked up that topic and did not only blog about it several times but implemented tool support with the the synthetic seek-clause of jOOQ. As the seek clause in jOOQ is excellent for relational databases, I’m gonna refrain now for calling the following the “very best way” of doing keyset based pagination and leave that to others.

So what is this about? The Spring Data Commons project – that is the base project for a broad variety of store implementations such as JPA, MongoDB, Redis and certainly Neo4j – added infrastructure support for keyset-based scrolling.

How does that look like from a Spring Data repositories point of view?

import java.util.UUID;
 
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.neo4j.integration.shared.common.ScrollingEntity;
import org.springframework.data.neo4j.repository.Neo4jRepository;
 
public interface ScrollingRepository extends Neo4jRepository<ScrollingEntity, UUID> {
 
	Window<ScrollingEntity> findTop4By(Sort sort, ScrollPosition position);
}

Keyset based pagination drops the notion of offset completely. It is dependent on an associated sort object, in this case given through the first parameter, the Sort object. As with all pagination efforts, we need to know how many items per page shall be retrieved. In this case, 4. This is determined by the derived finder method. The scroll position (the second parameter) determines the offset.

The above will be possible from the next Spring Data releases for the MongoDB- and Neo4j implementations. Some stores might offer additional support on their data access templates, Neo4j does not as of writing (we just added the feature just days prior to the current RC).

The beauty of the above is: For you as a user, the calling of this just works the same for all the stores. Imagine the following simple entity:

@Node
public class ScrollingEntity {
 
	@Id
	@GeneratedValue
	private UUID id;
 
	@Property("foobar")
	private String a;
 
	private Integer b;
 
	private LocalDateTime c;
}

And some test data (here, Neo4j is being used):

Connected to Neo4j using Bolt protocol version 5.0 at neo4j://localhost:7687 as user neo4j.
Type :help for a list of available commands or :exit to exit the shell.
Note that Cypher queries must end with a semicolon.
neo4j@neo4j> match (n:ScrollingEntity) return n order by n.b asc, n.a desc;
+-----------------------------------------------------------------------------------------------------------------+
| n                                                                                                               |
+-----------------------------------------------------------------------------------------------------------------+
| (:ScrollingEntity {b: 0, foobar: "A0", c: 2023-03-20T13:12:25.201, id: "c2c2ebe4-5a02-4d77-a53b-1abbc80aaad9"}) |
| (:ScrollingEntity {b: 1, foobar: "B0", c: 2023-03-21T13:12:29.201, id: "f4f84ed4-632d-431e-bb1a-b829bc2eaf5d"}) |
| (:ScrollingEntity {b: 2, foobar: "C0", c: 2023-03-22T13:12:39.201, id: "f1c088f8-0b7b-456b-99b3-db5a0199dec6"}) |
| (:ScrollingEntity {b: 3, foobar: "D0", c: 2023-03-23T13:12:31.201, id: "3b223485-e81b-4be8-8dbd-50277d313a8b"}) |
| (:ScrollingEntity {b: 3, foobar: "D0", c: 2023-03-23T13:12:31.201, id: "1f525d3d-cdfe-40a6-964b-1fbfc08fae99"}) |
| (:ScrollingEntity {b: 4, foobar: "E0", c: 2023-03-24T13:12:41.201, id: "572b780e-256f-41b7-87de-4a130bc3814b"}) |
| (:ScrollingEntity {b: 5, foobar: "F0", c: 2023-03-25T13:12:25.201, id: "457ec454-a9af-421c-a9c1-7f5ce95310c5"}) |
| (:ScrollingEntity {b: 6, foobar: "G0", c: 2023-03-26T13:12:55.201, id: "b423c34b-6952-4b73-b06b-d039cf7c7e7b"}) |
| (:ScrollingEntity {b: 7, foobar: "H0", c: 2023-03-27T13:13:00.201, id: "ca90cd25-a676-44d4-a4c2-2db32443bf2f"}) |
| (:ScrollingEntity {b: 8, foobar: "I0", c: 2023-03-28T13:12:57.201, id: "59a5dfb2-0e17-4eeb-aecd-95bb555e0117"}) |
+-----------------------------------------------------------------------------------------------------------------+
 
10 rows
ready to start consuming query after 55 ms, results consumed after another 3 ms
neo4j@neo4j>

All of that can be consumed in the simplest way like that:

import static org.assertj.core.api.Assertions.assertThat;
 
import java.util.ArrayList;
 
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.WindowIterator;
import org.springframework.data.neo4j.integration.imperative.repositories.ScrollingRepository;
import org.springframework.data.neo4j.integration.shared.common.ScrollingEntity;
 
class KeysetBasedScrollingIT {
 
	@Test
	void forwardWithDuplicatesIteratorIteration(@Autowired ScrollingRepository repository) {
 
		var sort = Sort.by(Sort.Order.asc("b"), Sort.Order.desc("a"));
		var it = WindowIterator
				.of(pos -> repository.findTop4By(sort, pos))
				.startingAt(KeysetScrollPosition.initial());
 
		var content = new ArrayList<ScrollingEntity>();
		while (it.hasNext()) {
			var next = it.next();
			content.add(next);
		}
 
		assertThat(content).hasSize(10);
		assertThat(content.stream().map(ScrollingEntity::getId)
				.distinct().toList()).hasSize(10);
	}
}

Here, the content is sorted by b in ascending and by a in descending order, 4 pieces at a time. The WindowIterator starts at the initial position and tanks in a window providing function. That window will scroll over the dataset. What queries are generated?

The initial query looks like this, retrieving n+1 elements sorted in the order specified:

MATCH (scrollingEntity:`ScrollingEntity`)
RETURN scrollingEntity 
ORDER BY scrollingEntity.b, scrollingEntity.foobar DESC, scrollingEntity.id LIMIT 5

Why n+1 elements? It’s an easy way to judge if there are more elements available to scroll further or not, without going through an additional counting query.

The next query looks like this:

MATCH (scrollingEntity:`ScrollingEntity`)
WHERE ((scrollingEntity.b > $pcdsl01
    OR (scrollingEntity.b = $pcdsl01
      AND scrollingEntity.foobar < $pcdsl02))
  OR (scrollingEntity.b = $pcdsl01
    AND scrollingEntity.foobar = $pcdsl02
    AND scrollingEntity.id > $pcdsl03))
RETURN scrollingEntity 
ORDER BY scrollingEntity.b ASC, scrollingEntity.foobar DESC, scrollingEntity.id ASC LIMIT 5

This has now 3 parameters:

:param pcdsl01 => 3
:param pcdsl02 => "D0"
:param pcdsl03 => "282ac053-c821-47f4-9a0e-0d12e0b91808"

The next page starts at an element whose b attribute is greater 3 or is equal to 3 and as a footer attribute lower than D0. If you look closely, the 4th and 5th poses a nice test case for our tooling and made it hopefully clear why we add the 3rd condition here: To add uniqueness to the keyset on which we paginate.

The Spring Data Commons and Spring Data Neo4j implementation of the keyset based pagination is quite sophisticated in that sense that it allows for different directions for different columns to sort by, in contrast to a simple tuple comparison (in the above case something along the lines where [n.b, n.foobar] >= [$pcdsl01, $pcdsl02, $pcdsl03] (also not a real tuple in Neo4j but a list comparison).

Above I presented using this feature with the new WindowIterator, but you can also control this manually like this:

import static org.assertj.core.api.Assertions.assertThat;
 
import java.util.function.Function;
 
import org.assertj.core.data.Index;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.integration.imperative.repositories.ScrollingRepository;
import org.springframework.data.neo4j.integration.shared.common.ScrollingEntity;
 
class KeysetBasedScrollingIT {
 
	@Test
	void forwardWithDuplicatesManualIteration(@Autowired ScrollingRepository repository) {
 
		var duplicates = repository.findAllByAOrderById("D0");
		assertThat(duplicates).hasSize(2);
 
		var sort = Sort.by(Sort.Order.asc("b"), Sort.Order.desc("a"));
		var window = repository.findTop4By(sort, KeysetScrollPosition.initial());
		assertThat(window.hasNext()).isTrue();
		assertThat(window)
				.hasSize(4)
				.extracting(Function.identity())
				.satisfies(e -> assertThat(e.getId()).isEqualTo(duplicates.get(0).getId()), Index.atIndex(3))
				.extracting(ScrollingEntity::getA)
				.containsExactly("A0", "B0", "C0", "D0");
 
		window = repository.findTop4By(sort, window.positionAt(window.size() - 1));
		assertThat(window.hasNext()).isTrue();
		assertThat(window)
				.hasSize(4)
				.extracting(Function.identity())
				.satisfies(e -> assertThat(e.getId()).isEqualTo(duplicates.get(1).getId()), Index.atIndex(0))
				.extracting(ScrollingEntity::getA)
				.containsExactly("D0", "E0", "F0", "G0");
 
		window = repository.findTop4By(sort, window.positionAt(window.size() - 1));
		assertThat(window.isLast()).isTrue();
		assertThat(window).extracting(ScrollingEntity::getA)
				.containsExactly("H0", "I0");
	}
}

The key classes in that API are org.springframework.data.domain.Window and the org.springframework.data.domain.KeysetScrollPosition. The first one contains data and information at which position the scrolling window is positioned at any time, the latter is a value object for the current keys.

Last but not least, the API has full support for scrolling backwards as well. Scrolling backwards requires inverting the order for each column individually. At the end of the post I’ll add a link how we did this in Spring Data Neo4j without going through the pain of fiddling around with strings. Just inverting the operator in the generated order is however not enough when going backwards. By doing so only, the window would jump right back to the beginning. As we only know the keys at which the window arrived and not the keys n positions backward, we can solve this issue by inverting the sort order as whole too, match and collect and than recreating the sort on the client side again (we have some ideas for a more sophisticated Cypher based solution, though).

Implementing this feature for Spring Data Neo4j has been quite a nice experience. All our query generation goes through the Cypher-DSL which is in essence a builder for Cypher. Here we made use of iteratively building conditions:

var resultingCondition = Conditions.noCondition();
// This is the next equality pair if previous sort key was equal
var nextEquals = Conditions.noCondition();
// This is the condition for when all the sort orderedKeys are equal, and we must filter via id
var allEqualsWithArtificialSort = Conditions.noCondition();
 
for (Map.Entry<String, Object> entry : orderedKeys.entrySet()) {
 
	var k = entry.getKey();
	var v = entry.getValue();
	if (v == null || (v instanceof Value value && value.isNull())) {
		throw new IllegalStateException("Cannot resume from KeysetScrollPosition. Offending key: '%s' is 'null'".formatted(k));
	}
	var parameter = Cypher.anonParameter(v);
 
	Expression expression;
 
	var scrollDirection = scrollPosition.getDirection();
	if (Constants.NAME_OF_ADDITIONAL_SORT.equals(k)) {
		expression = entity.getIdExpression();
		var comparatorFunction = getComparatorFunction(scrollDirection == KeysetScrollPosition.Direction.Forward ?
				Sort.Direction.ASC : Sort.Direction.DESC, scrollDirection);
		allEqualsWithArtificialSort = allEqualsWithArtificialSort.and(comparatorFunction.apply(expression, parameter));
	} else {
		var p = propertyAndDirection.get(k);
		expression = p.property.isIdProperty() ? entity.getIdExpression() : root.property(k);
 
		var comparatorFunction = getComparatorFunction(p.order.getDirection(), scrollDirection);
		resultingCondition = resultingCondition.or(nextEquals.and(comparatorFunction.apply(expression, parameter)));
		nextEquals = expression.eq(parameter);
		allEqualsWithArtificialSort = allEqualsWithArtificialSort.and(nextEquals);
	}
}
resultingCondition = resultingCondition.or(allEqualsWithArtificialSort);

Also, getting the comparator right is type safe:

static BiFunction<Expression, Expression, Condition> getComparatorFunction(
	Sort.Direction sortDirection, KeysetScrollPosition.Direction scrollDirection
	) {
	if (scrollDirection == KeysetScrollPosition.Direction.Backward) {
	return sortDirection.isAscending() ? Expression::lte : Expression::gte;
	}
	return sortDirection.isAscending() ? Expression::gt : Expression::lt;
}

Of course a keyset based pagination is not a total silver bullet and can also be outright slow if done wrong. For example you should make sure you have proper indexes on all columns you want to paginate over (which is true for Neo4j and relation, and I guess also for MongoDB). For Neo4j I shamelessly recommend using Neo4j Migrations to have proper control over your Neo4j indexes and constraints, across all Neo4j versions from 3.5 to the latest release in Neo4j Aura. Also, while you can totally go wild in your sort for the pagination and have dozens of columns, the conditions generated will grow exponentially. If this is what you want, be our guest but don’t complain 🙂

If you use this feature in a sane way, it will be faster than just doing offset based paginations in Neo4j and I hope you find it as useful as I enjoyed creating it together with Mark Paluch and Christoph Strobl.

| Comments (2) »

20-Mar-23


Weekly digest 2022-50

While listening to the beautiful Kahedi Radio Show, I’m having a hard time this week to put down words; I feel done for the year. Wrt music, one interesting documentary I watched last week was this:

It’s about the ever increasing prices of concert tickets and about the business of Ticketmaster, Livenation and Eventim. And wow, it’s incredible how everything in the world that used to be nice has been turned into a business and investment strategy. Makes me sad.

Last Sunday I clocked in my 12th halve marathon (21,1km) in 2022, running through frosty meadows, which was actually quite beautiful:



I finally found some nice and warm running bibs that fit me, the Saucony Solstice in Large. I have an inner leg length of roughly 84cm and I’m rather thin… Which makes most trousers kind of problematic. These fit well. And while I was at it, I spent a bit more money for trail-ish shoes, as I signed up for the Abdij Cross in January and than a full marathon in March, the Heuvelland Marathon. Looking forward to both very much. Let’s see if I can continue 2023 like 2022. Apparently I’m this year in the 1% most active users on Strava.

Java and Community

There will a small Java / EuregJug community in Aachen on the Christmas-Market, you might wanna watch this space:

I was super happy that Øredev published a video about my talk on GraalVM:

And because I just felt like it, I created a video demonstrating that todays debugging tools aren’t your uncles tools anymore. The code you see in my video is an actually issue we worked on and in that case, it really was essential to have proper debugging at hand. Of course, it still is fine to just investigate with a random Sysout, I often do this myself, but as everything: It’s just not black and white.

I was told by my kid, I should definitely ask you to subscribe to my channel, hit the bell and leave a like. Dunno in which order, though 😉

What else? I read up on a couple of things I wanted to understand fully. One of them had been Project Loom aka virtual threads in Java. To learn about this proper, read JEP 425 and than watch this presentation from Devoxx BE 2022 by Ron Presser and Alan Bateman first (those are the owners of the JEP) and than this by Mario Fusco, for some great insights when and when to use this stuff.

As always, do something nice for yourself on the weekend. Try to wind down and stuff not everything just in before the holidays. The latter is often pointless and doesn’t change a bit.

| Comments (0) »

16-Dec-22


Weekly digest 2022-49

This has been a somewhat bleak and dark week and I’m not feeling overall happy, I have to say. The weekend was pretty excellent with me doing the ATG-Winterlauf for the forth time:



Despite being now way too much over 40, I just got faster every time. 16km with a pace of 4:16. That is not bad at all.

Also, one of my kids turned 13 and wow, time flies. It was a nice day.

But things went down from there this week… 13%, 17% and now Neo4j as well. The company I work for also layed off a considerable amount of people. I wonder if this is the way forward… I found this piece on Stanford News that resonates a lot with me.

Also, direct friends and good acquaintances mourn the loss of a friend. Today, I’m gonna include a video to the quote in the subtitle above at the end of this digest. If we know each other and you need someone to talk, feel free to give me a ping.

Books

The things I enjoyed this week include the book “Life Cycle” by Frank Glanert. On the outside it seems to be another book about cycling and the love for bicycles, but it is actually a book about change, communication on eye level and making cities more human friendly places. But also about the pure joy and happiness nearly any bicycle and movement in nature can give. Highly recommended.

Java

After the successful release of Neo4j-OGM 4 last week we have been occupied with migrating a bunch of projects to JDK 17 fully. Of course they have been compatible and did run great on latest JDK, but since this week, both Neo4j-Migrations and Cypher-DSL are fully build with JDK 17 and both are proper modularised. I learned a bunch of things about the Maven shade plugin on the way, but after all, pretty enjoyable. I opened a PR on the GitHub release radar for Neo4j-Migrations, I hope this gets included.

Also, there was a nice discussion on Twitter after I mentioned the beauty of Java 17 pattern matching, especially with the instanceOf operator:

Anyway, I’m gonna call it a week now. Stay safe and do something nice recharging you. 12 more days until Winter solstice, I can’t wait for the days to become longer again.

| Comments (0) »

09-Dec-22


Weekly digest 2022-48

It’s getting bleak outside but as you might have guessed, it does not stop me from getting outside. Last week I managed to increase my Veloviewer maximum square to 40×40 tiles. That is kinda of an online game connected to Strava. I am a big fan of both services. The goal with the squad is to reach as many small tiles as possible from a location and create a square as big as possible. All running, cycling, hiking and swimming activities count and one small tile is one square mile. This is how my square looks like:



Most of it is cycling but also some hiking. Next year it’s going to get though. In the east the open coal mines start and in the south there’s a protected moor area and some tiles are… difficult to get. So I am gonna focus on the north and west.

Ongoing fires in IT world

Twitter, Meta and other big, add driven companies are dumpster fires these days, laying of workers in the thousands. While I feel sad for the people, it’s maybe for the better: Are these companies producing value or just issues? I don’t know. But the recession is reaching others as well. Others, that actually produce something tangible. Yesterday, Ash Kulkarni from Elastic published the linked letter about reducing teams by 13% of people. It feels like hitting close: Elastic is an open source company as well, with a lot of good people and many that I actually do know personally. I always assumed they are an economical wealthy company but alas, less growth than usual and not even losses are quite bad these days.

Earlier this year in June I read a book named “RCE”, by Sibylle Berg. RCE is the successor to GRM and I do recommend both of them:

The predictions in GRM about UK, social descent on the one hand and incredible wealth on the other felt like a prediction come true already and apparently, the things in RCE start happening, too. Why have we IT works thought at all that not having unions, not working together in many cases would do us any good? Why could we have thought just for a second that we aren’t expendables? Curious and frightened how things will continue.

More good reads

But more on the reading side of things. I finished “Sternkinder” by Clara Asscher-Pinkhof. A book from a Holocaust survivor writing about the Holocaust from children’s perspective. Going from Sternkinder to Sternhaus, Sternwüste and finally Sternhölle. It’s all short pieces and beautiful, but simple language, technical easy to read, but heartbreaking. I had to stop several times. But sure, let’s let idiot “musicians” like Mr. West talk about the “good things” Hitler and the Nazis did. Like my father used to say, Ich kann nicht soviel fressen, wie ich kotzen möchte.

Your weekly cup of Java

I am so happy: All PRs in Neo4j-Migrations that in one way or the other deal with JDK or dependency versions closed and dealt with: Upgrades to JDK 17, Spring Boot 3 and the latest and greatest Neo4j Java Driver done. The Quarkus Neo4j extension has just been released as 2.0 as well, including the latest upgrade and based on all of that, Gerrit released Neo4j-OGM 4.0 with support for Neo4j 5.

The last nights I did something else for a change, basically combined sports and Java and created this: Garmin Babel. It is a tool to work with your archive data from Garmin.com respectively Garmin Connect.

What? Let me show you:

I love Java and the ecosystem.

Let me point out some tech I use and the people that I know behind it:

And wow, I do love the modern HTTP Client in the JDK. Here’s the full async code to download a bunch of data:

private CompletableFuture<Optional<Path>> scheduleDownload(Activity activity, DownloadFormat format, Tokens tokens, Optional<Path> base) {
  return CompletableFuture
    .supplyAsync(() -> {
      // Randomly sleep a bit
      try {
        Thread.sleep(ThreadLocalRandom.current().nextInt(10) * 100);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
 
      var uri = switch (format) {
        case FIT -> "https://connect.garmin.com/download-service/files/activity/%d".formatted(activity.garminId());
        default -> "https://connect.garmin.com/download-service/export/%s/activity/%d".formatted(format.name().toLowerCase(Locale.ROOT), activity.garminId());
      };
 
      return HttpRequest
        .newBuilder(URI.create(uri))
        .header("Authorization", "Bearer %s".formatted(tokens.backend()))
        .header("DI-Backend", "connectapi.garmin.com")
        .header("Cookie", "JWT_FGP=%s".formatted(tokens.jwt()))
        .header("User-Agent", "garmin-babel")
        .GET()
        .build();
    })
    .thenCompose(request -> httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()))
    .thenApply(res -> {
      if (res.statusCode() != 200) {
        throw new ConnectException("HTTP/2 %d for %s".formatted(res.statusCode(), res.uri()));
      }
      try {
        var suffix = switch (format) {
          case FIT -> "zip";
          default -> format.name().toLowerCase(Locale.ROOT);
        };
        var filename = "%d.%s".formatted(activity.garminId(), suffix);
        var targetFile = base.map(v -> v.resolveSibling(filename)).orElseGet(() -> Path.of(filename));
        Files.copy(res.body(), targetFile, StandardCopyOption.REPLACE_EXISTING);
        return Optional.of(targetFile);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    })
    .thenApply(path -> {
      path.ifPresent(v -> System.err.printf("Stored data for %d %s as %s%n", activity.garminId(), activity.name() == null ? "" : "(" + activity.name() + ")", v.toAbsolutePath()));
      return path;
    })
    .exceptionally(e -> {
      var prefix = "Error downloading activity %d ".formatted(activity.garminId());
      if (e.getCause() instanceof ConnectException connectException) {
        System.err.println(prefix + connectException.getHttpStatusAndUri());
      } else {
        System.err.println(prefix + e.getMessage());
      }
 
      return Optional.empty();
    });
}

From scheduleDownload.

Anyway, if you find michael-simons/garmin-babel useful, please let me know. I had a lot of fun writing it, but I realize I am not 30 anymore and hacking away 3 nights straight in a row took its tool. Super happy that my employer Neo4j continued with the 4th Neo4j Global Wellness day in 2022 today and I just can sit down with a cup of coffee and write this piece here.

Before I leave, todays featured biking picture:



Taken during the bleak and dark morning hours, accompanying my kid to school. In that sense, do something nice, not only over the weekend, but also during the week. Time is too short to waste it.

| Comments (0) »

02-Dec-22


Weekly digest 2022-47

And another week gone. Time flies. It seems that I manage to make a habit out of note taking and writing about it later. I notice a big change in my days not having the urgent need to tweet everything all at once all the time. As shown in my last digest, I put the screen-time widget onto my iPhone home screen. I’m happy seeing a big drop in daily average screen time on the thing. In the meantime I have not only removed the Twitter app from my home screen, but also deleted the app. It’s much more inconvenient to use the web page, as I logged out from it, too.

Anyway, last weekend I started with the most extreme half-marathon (13.1mi / 21.1km) in 2022. 500m of gained altitude in one run, under 2 hours. I was completely toast after that, despite the fact that I did a half-marathon each month this year. Anyway, I am aiming for the 2000m running altitude badge again this month, but that’s a though one.

So what was ongoing this week? I have binged watched season 2 of Inside Job and I am absolutely loving this to pieces:



Reagan Ridley noting on his father becoming the head of Cognito: “This is going to be the most globally damaging midlife crisis since Elon Musk.” which has been written like a year ago according to… Twitter. Continuing with that, the video I enjoyed most this week was probably done by Adam Conover and has the friendly title “Elon Musk Is An Idiot (and so are Zuck and SBF)”:

I would not call those folks dumb, which I don’t think they are, but selfish, ignorant morons and man-childs with too much power in their hands. Anyway. If your interested in more on the twitter mess, I enjoyed that article from the parted head of trust and safety at Twitter which confirms the fact that Twitter will eventually be in real trouble in Europe and hopefully that will get Musks ass. A more personal stance can be found by Matt Tait: Twitter was special. But it’s time to leave. And with the Miro-Board from Twitters code review I can jump right into tech, I guess. The Miro board gives an interesting high-level view about the services at Twitter, especially the “Mixers” that basically create the curated timeline. In that context, I totally dug this thread by Jon Bell on UX design on social network in general and comparison what’s on Mastodon vs Twitter. Brilliant stuff, worth the read and makes you think, if you are sometimes the loudest voice as well and ignoring a more silent minority.

To more positive stuff: Congratulations Spring-Boot-Team: Spring Boot 3.0 Goes GA 🎉🥳🍾🎊. Crazy, the project is now nearly 8 years old and what a ride. I was wondering the other day: Back then, the idea was part about self-contained application but also introducing better best-practices than the habit of copying and pasting various XML configs and setups from StackOverflow into Spring projects until they worked with the idea of auto configuration. These days, I find tons of (auto) configuration classes when working on tickets and issues all the time which at best are redundant but more often, dangerously wrong. At times, I feel like we have reached (another) full circle (Please note, that is *not* a negative statement about Spring Boot at all, it just seems to be the way of things these days).
Anyway, in case you are providing custom starters for Spring Boot, make sure you stop using spring.factories together with @Configuration classes for your auto configuration already in Spring Boot 2.7, as that approach will stop working in Boot 3. Here’s a commit that shows the needed changes: refactor: Migrate to @AutoConfiguration. Makes things a lot clearer, I like that.

Speaking about cool libraries: I use Picocli A LOT. And every time I think I must have found that thing that won’t work ootb, it does. This time, hidden options, parameters and subcommands. Sweet stuff.

Also, I wrote kind of a beginner post on Java Records and name clashing: While stuff like that can feel redundant to experienced developers, it isn’t in reality.

It’s about community

In terms of community and fun, this week has been brilliant. I visited Bauer + Kirch to talk about software, proper architecture and a bunch of other things and it might be that we can do an EuregJUG event there next year. They are looking for Java folks among other roles. Might be interesting for the reader.

This weeks featured biking picture was taken on my ride to Franz who really pulled of a public viewing for Karl Heinz’ talk about Maven 4 (essentially this talk). We even snatched Karl Heinz after the talk and enjoyed more French fries by the master of the industrial deep frier himself 😉





Last but not least, next week is INNOQ technology day. Fantastic opportunity to listen to excellent speakers for free. I would check this out!

Stay sane and remember to use the weekend to wind down 🙂

| Comments (0) »

25-Nov-22