Ilyass Bougati

The Annotation That Took a Month

June 2, 2026 - 11 min read

Article cover image

If you read my last article about DeepDame, you might remember the small promise buried at the very bottom. There was a P.S. about a @RedisListener annotation, and a line saying that was a story for another article.

This is that article. It is also the only serious open source contribution I have made so far, so I want to tell it honestly: the parts I am proud of, the parts where I was completely out of my depth, and the part where I leaned on AI so heavily that I spent months afterward wondering whether I had actually done anything at all.

The Annoyance

DeepDame is a distributed checkers game. We ran the backend on Kubernetes and scaled it horizontally, which sounds great until you remember that checkers needs exactly two people in the same conversation. Player A connects to replica 1. Player B connects to replica 2. Player A moves a piece, and replica 1 has no idea how to deliver that move to player B, because player B is not its problem; player B belongs to a completely different pod.

WebSockets alone do not solve this. We could have hand rolled socket connections between the servers, but that idea lasted about as long as it took to say it out loud. We already had Redis in the stack for caching, so Redis Pub/Sub was the obvious answer. One replica publishes a move, every replica subscribes, the right one forwards it to its client.

Wiring it up is where the annoyance started. In spring-data-redis, you do this by hand. You create a RedisMessageListenerContainer, you create a MessageListenerAdapter for each listener, you register each adapter against its topic, one call at a time. By the time I had a few channels (game moves, chat, game over, invitations, lobby updates) my configuration class looked like this:

container.addMessageListener(gameMoveAdapter, new PatternTopic("game-updates"));
container.addMessageListener(gameChatAdapter, new PatternTopic("game-chat"));
container.addMessageListener(gameOverAdapter, new PatternTopic("game-over"));
container.addMessageListener(generalChatAdapter, new PatternTopic("general-chat"));
// and so on, by hand, forever

I had just finished using Kafka in another project, where listening to a topic is a single @KafkaListener annotation on a method and you are done. So sitting there registering adapters one by one, I had a very specific thought: this is not how Spring Boot is supposed to feel.

Spring's whole personality is convention over configuration. You annotate a method and the framework wires up the plumbing. So why was Redis the one corner where you had to do everything manually? I went looking for the annotation. It did not exist.

Scratching My Own Itch

So I built it. I wrote a small Spring Boot starter that added a @RedisListener annotation: put it on a method, point it at a channel, and the starter handles the container, the adapter, and the registration for you. I published it to Maven Central, then I went back and rewrote DeepDame's messaging layer on top of it. The seven manual registrations collapsed into seven annotated methods. It worked.

@Service
public class MessageReceiver {
    @RedisListener(channel = "test-channel")
    public void handleMessage(String message) {
        System.out.println("SUCCESS! Received message: " + message);
    }
 
    @RedisListener(channel = "test-channel")
    public void handleMessage(MessageDto message) {
        System.out.println("SUCCESS! Received message: " + message.getId());
    }
}
 

I could have stopped there. I had a published package, it solved my problem, and it was honestly a clean little library. For a while that was the end of it.

How Hard Could It Be?

Then I had the thought that gets me into trouble every time: how hard could it be to add this to spring-data-redis itself?

My last article ended on a lesson I had supposedly learned from DeepDame: never bite off more than you can chew, unless your goal is to learn something. Apparently I took the second half as permission, because I decided to take my little annotation and propose it to the official Spring repository.

I want to be clear about the gap here. Writing a starter for myself is one thing. Proposing a feature to a library that millions of developers depend on, maintained by people who have been guarding its design for years, is a completely different sport. I did not understand that yet.

The First Reply

I did not start from a blank page. Digging through the existing issues, I found one, #3258, proposing a type-safe take on the same problem I had been wrestling with, and I added my own comment to it. Mark Paluch replied to me directly. He agreed it overlapped with a much older request, pointed me toward how Spring already solves this for its other messaging tools, and made clear that the hard part was never producing an implementation. It was working out a model that actually made sense and felt natural to use. Then he closed the issue as a duplicate of #1004 so the conversation could continue there.

I did not pay attention to that number at the time. I would only realize later that #1004 had been open since 2015.

On #1004, another contributor, onobc, picked up the thread and gave me a generous, concrete starting point. Rather than inventing something from scratch, he pointed me straight at the family of annotations I had been thinking about all along: @KafkaListener, @RabbitListener, @JmsListener. Study how those are built, use the simplest one as a blueprint, and sketch out a high-level design before writing real code.

There was something quietly validating in all of this. The annoyance that started everything was "why is there no @RedisListener when @KafkaListener exists?" And here were the people who maintain the library telling me that this exact family of annotations was the right mental model. My instinct had been correct. I just had a decade of other people's context to catch up on before I could act on it properly.

Out of My Depth

onobc had handed me the map. Mark was the maintainer who walked the territory with me. He is sharp, exacting, and he wanted the feature to be right, not just functional. That is exactly what you want from someone protecting a public API, and it is also why the next month was the hardest technical stretch I have been through.

The hard part was never simply writing code. It was meeting a level of exactness I had not worked to before, and before any of that, just understanding what Mark wanted. Echoing onobc's advice, he started by asking me to sketch out the declaration side of the change: not the implementation, but how the application code would look from the developer's point of view, so we could work backward to figure out which components to build and how to handle invocation, exceptions, and return values.

But it was never just one comment. I would reply, and a new comment would come back, often using precise, specific vocabulary (endpoint registrars, content negotiation, return value handling) that I did not fully have yet. So I would go decode that one, reply again, and get another. A lot of that month was simply this loop: looking terms up, and yes, leaning on LLMs to translate the language of someone who lives inside this library every day into something I could act on, just so I could keep up with the next message.

And the exactness did not stop at the concepts. It ran straight into the code. Across the review I made tens of changes, and more than once that meant tearing down what I had already written and restructuring it because the shape of it was wrong. A comment was rarely "fix this line." More often it was "rethink this," and I would.

That loop taught me something about why so few people contribute to a project like Spring. The barrier is almost never "can you write code." It is that contributing means stepping into a maintainer's accumulated decade of context and idiom and being expected to operate in that language. I am not going to dress this up: I spent most of it a step or two behind, barely keeping pace. The only thing I really did right was not quit.

Merged

After weeks of that, the message that meant the most was the simplest one: the maintainers told me they would take it from here. After tens of revisions and more than one full rewrite, someone with the keys to the project was stepping in to carry it the final stretch. I am not sure anything in the whole process felt better than waking up to that.

Across two pull requests (#3303 and #3321), it got merged. The annotation is part of spring-data-redis now.

The detail that still does not feel real is this: the gap I had stumbled into while wiring up a student checkers game was that same request from #1004, the one that had been sitting open since 2015. A decade of "it would be nice if" finally carried over the line. When Mark merged it, he called it a long envisioned feature and a massive contribution, and said it was on track to get full support.

I deprecated my own package and archived its repository shortly after. The official version made mine redundant, which is exactly the outcome I had been working toward, even if archiving something I built with my own hands felt strange.

What I Actually Did

For months afterward I kept circling one uncomfortable question. I had used AI so much (to write code, to decode terminology) that I genuinely did not know how to account for my own role. If a tool did that much of the typing, did I actually do anything?

I have made my peace with it, and the answer is not a triumphant one. It is just honest.

The AI never felt the annoyance of registering those adapters by hand. It never had the thought that something was missing from a framework I love. It never cared whether some stranger's Spring configuration, someone I will never meet, got a little cleaner. It never sat in a confusing review thread for a month refusing to give up on understanding a maintainer. And it was never the one accountable for the design when Mark asked hard questions; I was.

Mark put it better than I could, in a comment I keep coming back to. He said that writing the code is not really the problem to be solved, because you can throw a few sentences at an LLM yourself. What matters when you build for a broader audience is listening, understanding, and activating creativity to address a genuine concern.

That is the part that was mine. The noticing, the idea, the care, and the stubbornness to stay in a conversation I barely understood. The tool did the typing. I did the rest.

I am 23, and this is my first serious contribution. I am not going to pretend it makes me an expert in anything. But I learned that the scarce skill was never producing code. It was caring enough to see a small annoyance as a real problem, and then being willing to be the dumbest person in the thread for a month until it was solved.

That, it turns out, is still my job.

P.S. My Name Above the Annotation

I made this whole journey sound hard, and it was. But I will be honest about something else: I still feel a small jolt of pride every single time I open the source and see my name sitting right above the annotation.

/**
 * ...
 * @author Ilyass Bougati
 * @author Mark Paluch
 * @since 4.1
 * @see EnableRedisListeners
 * @see RedisListenerAnnotationBeanPostProcessor
 * @see RedisListeners
 */
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(RedisListeners.class)
@MessageMapping
public @interface RedisListener {
	//...
}

And then there are the official docs, which now present this annotated endpoint approach as the simplest way to receive messages asynchronously: any method on a managed bean can be turned into a Redis listener. Seeing that written in the Spring documentation, in the same calm, matter-of-fact tone as every other feature, still feels surreal to me.

Because in the end, after the month of revisions and rewrites and vocabulary I had to look up, this is all it looks like to the person using it:

@Component
public class MyService {
 
  @RedisListener(topic = "my-channel")
  public void processOrder(String data) { ... }
}

One annotation. No wall of beans. The hard part was mine to carry, and the easy part is theirs to enjoy. I think that is exactly how it should be.