From Chaos to Cohesion: Building Modular Monoliths in the Real World by Patrick Baumgartner SIO26
About this talk
In this talk, Patrick discusses the concept of building modular monoliths as a way to transition from chaotic codebases to cohesive, maintainable applications. He emphasizes the importance of structure in software development, noting that unstructured code leads to complexity and inefficiencies. Patrick outlines a five-part framework for improving software organization, addressing issues such as code erosion and the pitfalls of adopting microservices prematurely. He introduces the Spring Modulith framework as a tool for defining module boundaries and enforcing best practices, enabling developers to incrementally improve their codebases without freezing features or rewriting applications. By demonstrating practical steps and strategies, he encourages teams to adopt modularization and emphasizes the ongoing journey of software refinement in the face of evolving technology demands.
Full transcript
Um, it's great to be back at Spring I/O because always when you see that your Wi-Fi connect automatically, it means like you're home, right? And this is like what happens to me here again. So, which is actually like really awesome. But I want to talk about something else. I want to talk about from chaos to cohesion and building modular monoliths in the real world and show you
some ideas like how you could bring your application into something which is probably more structured, something you really want actually like in your application. So, I'm Patrick. I'm from Switzerland. I'm a technical agile coach. I help software teams to build better software and also deploy it faster, of course, with better quality. And I think actually also like in the age of AI these days, it's really necessary
that certain types of applications will need some more structure that is staying more maintainable also in the future. Of course, you know that we are going to create a lot of like disposable software these days, but there is of course some applications which need to stay longer, which we need to maintain over time. And I'm pretty sure like in the next 5 years or so, we'll have
quite a lot to do to clean up the mess we are going to producing these days, right? So, that's what I'm doing basically like working with people on on real problems building like software with humans and also lecture at the University of Applied Sciences and also I'm involved in Switzerland like in a few community things like also organizing local conferences and stuff like that. But that's just
like my background and what I'm doing. So, I want to go with you through the next 40 in the next 45 minutes through those topics. So, I have structured my talk in basically five sections. So, we'll have a look at the problems at the beginning why actually like a code base is going to erode. Then actually like we talk about the vision what we actually want to
have. Then we go on a journey and we we go through seven steps in my presentation how you could actually put more structure in your application. Then we're talking about the next steps what you could do afterwards if you have built a modular monolith. And then actually in the end, the idea would be to take some takeaways what you could do actually like Monday morning when you're
back at work how to like use the things Right? So, let's talk about the first thing the problem statement basically. Why structure is eroding? And I mean, who of you have ever felt lost in a code base? I guess all the times, right? So, when you saw a new code base, it's difficult to get into it the bigger the worse. Um, it's quite interesting to see that
in 2008, Stripe was conducting a study developers worldwide how much time and energy was waste basically like with reasoning about bad code. So, what code is actually doing? They came up with the number of almost 10% where they said like developers are looking at this bad code. These were not bad developers, but like we know that software erodes over time and the structure just like disappears because
of many reasons. And you have been there probably adding a new feature to your application and you have to touch about 15 packages, right? So, that's just too much. That shouldn't be the way. Maybe you touch certain parts of your code base which was just not meant to be touched because like it serves a different purpose. And we also could say like nobody really planned for this,
right? It just happened in a way. So, this is actually like how is a a monolith decaying over time. Of course, when you start the project at the beginning, it's quite simple. Everything is clean. You have a small team. You have a good structure and so on. Everything is like ideal, hopefully. That's how we start. But then actually over time, your application is growing. New features are
added. New requirements are putting into place. We might have new developers joining the team and you have some pressure to chip some features, right? So, what happens? Yes, the boundaries start to blur over time. You know, you just do an import of something in your application and you know it's not perfect, but it's just faster. Not faster during execution, but faster for delivering the feature, right? And
over time, this actually piles up and it becomes a big ball of mud. And that means like in the end we have like a structure as soon as you're going to try to like um touch some code base, it's already risky and you don't want to touch it anymore because you don't know what's going to break, right? So, actually the problem is not the monolith. It's the
missing structure. And I'm sure many of you or probably all of you have been there actually. So, even if you think like if I would tell you a story from the field, if you think of a medium-sized application with just like 80,000 lines of code, about four teams involved from time to time in the application with like 5 years of history and like the classical way of
structuring the application how you would see it in very much all the demo applications package by layer, it was actually just like a mess. And we applied this like checks to find out what's going on in the application and we have found out about 87 violations which shouldn't have been there. Cycles in the code base we found like four of them. And of course, we had this
like got mode package which just contained everything else, right? But actually to be honest, like what I'm going to tell you here is quite simple because you can start within a few days. So, we did actually like the first module like in the first 2 days we cleaned it up. And then over several sprints, we improved over time until we found like the right boundaries and we
had like five different modules. And I'm going to show you later how we did that or how you could do that, too. Of course, in the end we ended up with zero violations. We had no cycles anymore. And the best thing was it was CI enforced. So, that means like new no new merge requests actually like with violations got added to the project. That's like heaven, right?
Isn't it? So, and even for the the product people, it was awesome because like we did not do a new rewrite. There was no feature freeze involved. We just did it gradually over time and it was so much easier and we shipped features during the migration. And that's actually like how it should be. We can still like run the project. There is no feature freeze. We can
always like deploy something to production and we are always on the safe side and we can earn money with that. But the scariest part was actually like running the test because we didn't know what to expect, right? So, but after that, it was just work cleaning up the code base step by step. So, now let's dive into slightly similar topic. You could actually say like if you
have a big ball of mud to bring in structure, there could be the possibility to actually use like microservices. Oliver Droopel has talked about this morning in the keynote about that. And during a certain amount of time, it was normal actually like to introduce microservices because it provided those kind of boundaries we needed. But then we learned actually like it comes with a tax with a distributed
system tax. You know, network latency, partial failures, eventual consistency. These kind of things are difficult and also to operate the whole thing, it's not that easy. You have also organizational overhead, right? Team boundaries, API contracts, versioning you need to apply and you may not want to go there, right? So, the problem is really like that many teams adopt microservices too early before they need them. And I
would love in my personal opinion, I would love to learn first like how I need to do it with a plan to go there if I really need it because microservices are really expensive and you need to put a lot of like resources into it to do it right. So, here again, the message is actually like the answer to a messy monolith is not distribution, it's structure.
Now, of course, if you look at the code you probably have seen package by layer before. But I want to come up with a metaphor which I find fascinating. Think of a grocery store. If products are sorted by color then by type, how would I find my blue socks? Right? It's quite difficult. It's easier to go to the section of socks and find the blue ones there
than the other way around. So, that's what we do in the code base, too. So, if we are going to build modules in our code base, we are not structuring by layers with controller services repositories and so on. We are ordering by the modules like like the capabilities of the domain like order domain, like the inventory domain, like the customer domain. It's all the way easier to
group the things how they belong to. And this is basically a huge trap we fall into when we are building like a monolith or how how we have done that in the past. And over time, of course, like we found out as as software development people that it's probably better to sort our code differently, to organize it differently. So, modularity is not a new topic. If you
go back a little bit in history, we have known that modularity is actually like a thing since like more than 50 years. So, David Parnas has written in a paper on the criteria of to be used in decomposing system into modules in '72. That's quite long time ago. Tom DeMarco then actually like in a book about structured analysis and system specification, he adopted the idea of cohesion
and coupling we all know these In around 2015, Simon Brown said actually like if you can't build a structured monolith, what makes you think that microservices is the answer? And in a recent talk even like Hamelin Henny said, the missing word isn't monolith, it's actually messy, right? So, the thing actually like what things have changed over time is also not the perspective how what we perceive as
good software, it's the tooling. So, if you think actually like how you could modularize code in Java, you would actually use the modifiers like public, private, package protected. These are the tools you have, right? I'm not talking about the modularization in Java because that's a total other story. But now actually like when we go into and use like tools like ArchUnit or Spring Modulith, we have new
tools to enforce like proper boundaries and still can like leverage the the public modifiers for example in Java because we need it sometimes, especially if you need to like import code which is maybe like in a subpackage within your model. that's the thing. We have like new tools. The principles are not new, but we need to in a way to enforce it. So, what does modular actually
mean? And there we meet actually like the Spring Modulith framework which is actually quite a nice extension for Spring Boot. It's not a new framework, it's just an addition to it. And actually to be honest, if Spring Modulith would not be further developed like somewhere in the future, future, if you have this kind of fear, then it's quite easy to remove it again and you just have
a simple Spring Boot application which is deployable. But on the other hand, Spring Modulith brings in some nice extensions. So, it provides conventions for module boundaries based on package structure. It's just one convention, but you can also customize that if you want. It offers verification, documentation, and also runtime support. And of course, it just works with regular Spring Boot apps. So, that's why it's so nice and
simple and small and just gives us some additional guidance actually like how to build a better Now, of course, if you would go like from left to right, you could say like we start with a messy monolith and we going to want to go maybe into the direction of microservices, but we are not sure yet. You could actually like move over to the middle and say like
let's stay with the modular monolith where we have clear boundaries, a single deployment, and if we want to extract the module, we could if we really need Because as I said before, there will be an additional tax we have to pay if we go to microservices. What I'm not saying here is you may have heard of a concept called like self-contained systems. And a self-contained system might
be still like my monolithic application. That's still good, right? But what we don't want to have is like one messy big thing out there. So, we try to be like not on the left, but also not on the right side, but adding more structure, more on the middle side where we have like clear internal structure. So, when you're going to choose a modular monolith in the real
world, and that's our close where is the title from my talk is coming, is actually like the thing that you say like I have a team size which is small to medium. It makes sense for us as a group to like create nice boundaries, nice modules. Maybe the application is not so huge, and we don't know where we are yet. So, the domain boundaries are still moving
and we have to find out what makes sense. So, if you start from scratch for example and you're adding features over time, you really have to think of like where you want to put the things. If your deployment simplicity matters, if you just want to deploy one application, then that might be also good reason why to have like one single deployment. And then actually another thing is
what I have seen with several teams is that actually like adding Spring Modulith afterwards to an application helps really like to evolve and improve an existing code base. It's not something you need to have in place when you start from scratch, which is actually also like quite nice. So, you see actually like the adding structure in a living product actually like could be easily applied and there
is not much magic behind. And we will go and see some features a little bit afterwards. Now, let's talk about like the vertical slicing approach, like grouping it by domain, not by layer. If you talk about modules, we would actually say a module has a public API and it has internals. We have dependencies, but also we want to group by business capabilities. So, that's what we have
also what we are going to see as well like in the presentation. So, that the business topic actually like is maybe like one of the things we want to group things. In Spring Modulith, we have a definition for the provided interface which means like in a module, we have like public beans, public interfaces exposed as an entry point into the module, or we are going to work
with events which are also part of the public interface. We have some internal implementation which is actually hidden and which nobody else should actually use. And then on the other hand, you have in a module also like the required interface where you say like what's my public interface I'm consuming on the other side, what are the events I'm listening to. So, we have this kind of definition
what I what my module produces to the outside, but also what it consumes. Now, in a Spring Boot application by definition, a module is basically like a top-level package under the Spring Boot application annotation. And we have seen that on the previous slide with the with the approach so that a module is just defined by that. And subpackages by default are not exposed and we are going
to see that also in the demo. Now, what we actually want to try to avoid is that somebody actually like takes a forbidden path and directly accesses some code from another module with not going through the public API. And this is something we want to avoid. Just because it's faster, you know, as I said before, it's not a reason. That that's not proper and it might add
some technical depth, architectural depth to Now, if you're going to specify things in a Spring Modulith application, you could actually go a step further by using the application module annotation. It's not needed, but it helps me to add additional conventions when I need them. And as you can see in the examples, I can even restrict that a module, like now my order module, is basically consuming consumer
and inventory module and it's not allowed to actually like consume any other modules. So, I can restrict that for the verification. Inside an internal package because SPI is an internal package, I could also expose certain things as a public thing. So, and there is the concept of this named interface where I just can expose some single thing with a specific name to the outside as a part
of the public API. Maybe because of some reasons I don't want to move it to the top-level package. And there might be modeled modules also where say they're totally open. We don't care about like who is going to access it and how, right? And that might be something for a shared module. You could say this shared module is like shared code. It's like the shared kernel of
an application. And it should not be that big because like if you have like a dumping ground where you just put everything left over in there, that's not really bad, but that's really bad. You might want to have a module, maybe a common module or a shared module which is staying small and you just add things like value objects, maybe some calculations in there, but which is
not related to the domain methods or the domain activities you have inside a module. So, if you think of like you use the same approach for doing something twice, I would consider even duplicating the code than choosing the wrong abstraction and moving it into the common module. So, I try to keep my shared code or my shared module as clean as possible, as small as possible. So,
otherwise it grows and that part becomes the big ball of mud again. So, we don't want to do that either, right? Right. Good. So, let's go like through the seven steps where I can show you some examples and how we are going to add structure to So, imagine as we said before, we are going to going to rewrite the application. It's basically like just like moving code
around and that's the first step where we move and organize the code like in a different package structure. Your favorite IDE might help you with that in an easy way with doing like safe refactorings. Nothing is breaking, still compiling, you still can deploy it. We just move code around. Then actually like we're going to add verification step to verify the Maybe at the beginning I'm not verifying
everything. Well, maybe the first time yes to see how bad it is and then I try to like verify my first small module. I'm going to fix the violations and then I tighten the boundaries which means like I maybe go and add another module to it and I verify that again and I fix the issues again, right? And I'm I'm always like in this constant loop of
like fixing violations, making my test green and then actually like tighten the boundaries even more um until I have like the ideal world for my application. the question actually which arises is how do I find the proper boundaries for my modules? And there I have a few ideas um here on my slide. So basically you could say we are aligning the modules with business capabilities. That's just
like the domain thing. That's like customer, that's inventory, that's order, whatever, right? That's maybe not so simple, but maybe you want to look at the data. So if you look at the data or the database, you could also see like what belongs together and what consumes your data and maybe that's like your module boundary because like sometimes and and that's definitely not what you want that you
have like a modularized application which is accessing the same tables, right? So you of course need to refactor the database as well, but if you can't do it more, then maybe that's like the boundary for your domain, right? Another approach could be actually like that you define the the boundaries um when you follow the team or your department. We if you think of like an application consists
of different topics, you're doing something like for for an for a company where different departments are involved, maybe it's a good idea to just like modularize the thing for one department, another module for the other department and so on. Because if you're going to change a module, you don't want to break somebody else's code, right? And they would actually hate you for that. So therefore sometimes it's
actually good if you think of like how you define your teams um in your company and that's also how you structure your code. So basically in a way like Conway's law, right? So yeah. Of course you can also do other things like um asking domain experts, read existing documentation, maybe use some other tools actually like to visualize your code so that you know what's already there, which
gives you maybe a better idea how to structure things. So if you now think of my code base and I will in a in a second then jump into my demo, um you can see here that actually like my order service is um changing the quantity on the stock. And basically like the inventory and the order domain are two different things and that means my create order
actually like works on creating the order, but also it like deducts the things from the stock, which absolutely feels wrong. We have here a um a violation of the boundaries and um in a regular application in regular Spring Boot application, you wouldn't see that. Okay. if we want to like do this and thinking now still of modules here, instead of directly accessing a repository which is basically
part of the the internals, we could introduce something like a inventory service for example, where we say the inventory service is part of the public API and we could access it through that. Not taking the shortcut, right? That could be one way. In Spring we would inject a Spring Bean with a with an interface which is part of the public um API of a module. That could
be one way. So if we do that, that's more or less like the simpler approach, then we go and to write something like um I call the whistleblower because I like the idea of like I'm having one test which actually like finds everything in my code base which is going wrong and nobody could bribe this person. He's always telling the truth. So by adding actually like this
um verification call, what we do here and you execute it, you will immediately see what's going wrong in your application. So that's usually like my verification step and then I need to go back and fix So typically um your structure is broken and you will find violations for different topics. So that could be something like you have illegal cross module access, so you're accessing things which you
are not allowed to with your model. You have your cyclic dependencies or maybe you have declared that you're only allowed to consume certain dependencies from certain modules and they're not listed So the test will blow up basically, it's red and you will see what was done wrongly. Okay. So let's have a look shortly at some source code and I'm just switching to um the IDE and I
have some examples here, but I'm not going through all of them. Just like I'm going to show you a few of them. So the first one is actually like when I open it up, I have here already things um organized in a module. So I have like my inventory module, I have my order module. Um if you would look at the code for example, certain things are
in your public because they need to be. They're used inside the order service as we have seen it before on the slide, but it still compiles. That's just a regular Spring Boot application. Now in the next step, we're going to add some dependencies and we could see that, let me try this way, we could see that that we have like the Modulith Starter Core in there and
we also add the Modulith Starter Test. And this is actually just two dependencies you add and we have added as well the verification. So actually like verify the verify part here. So this is a test which would be executed. We'll see that in a minute. Now in IntelliJ and Ollie showed that also this morning very well, you have the modules and IntelliJ shows up actually like the
red locks and the green locks. Red locks means it's an internal package, nothing can be exposed. If it's a green lock, things can be exposed because they're part of Now if you're going to look at my code here and just pick for example the order service, you would see the order service here has no public modifier, so it would not be exposed at all, but I still
can keep it on the top level. So Java will help me there and it won't be part of the public API even though it's on the top level package. But what I like a lot is IntelliJ shows me here a violation. It tells me I'm not allowed to do this. So I see that it's not a compiler warning or so, it's more like a tooling because IntelliJ
is bundled with the Spring Modulith plugin. Um it's already installed by default and it will show me that there is something going wrong. And if I'm going to execute my test here and that's what I do right now, you will actually see violations are popping up as well. Right? And these are basically like the same ones um you would see in the IDE. So IDE gives you
early feedback before you actually execute the test. Now if we go a step further for the next one, I have like decoupled that and we get also like into the topic a bit later um with the slides and I have added actually like in the order module here. Let's let's uh show it like show it like this. Um I've added a event. So I have an order
completed event. So if I open now my order service, I'm going to use the application event publisher from Spring Framework and I published an event actually like in the application. And now I decouple the direct call to the order module by having some events thrown in the application, which actually like brings in way more flexibility and the only thing now which is part of my public um
domain is actually like my order completed event as you can see here also with the public keyword, so I can import it on the other side from Java. Um in the inventory, also like in the internal module basically like in the hidden part, I have this kind of like application module listener imported um where I basically like work on the events and by consuming the event, I
can now like deduct one thing from the stock and save it again, right? So this is like now not the direct call anymore, but we made it like asynchronously. And that means like if I would go and execute my test, um it would be green. But let's go back to my presentation. So um by decoupling now the event decoupling with events actually like my code got way
simpler, but you might think now it's asynchronous. well, it depends, right? Uh we we can actually do that with Spring Modulith and that makes also in in certain cases really sense. So but we get there um in a few seconds or minutes. So, we see now actually like that we do not have a dependency anymore on the internal inventory package. And we just have a new thing
introduced to the event for your completed from the order module. So, I put like the event there where it belongs. The inventory listener actually like subscribes via an application module listener, which is a spring module if thingy, uh which is a transactional event listener. It's async and it's running its own transaction. So, that means actually like what we are going to see is we could store a
thing in the database an order. The order is stored and then basically like working on the inventory runs in a separate transaction, right? That's maybe a good thing because if something with the inventory goes wrong, the order is not rolled back, right? And we just saw here actually like we use now um events to simplify and and strengthen So, by adding this application module listener, there is
another a huge advantage because now you could say like let's add some notification to it and that means like um we could have a notification module there which sends out emails. And we just basically like subscribe to the event bus for those events we want to without having that direct coupling inside your order service. So, that makes life also way easier. So, breaking apart the code into
things um we have some strategies. Of course, we can move the code where it belongs to. Then we don't have like these strange dependencies between modules. So, put the things into the right place. Publish an event, then you can actually like decouple things quite a bit. Or you could actually like expose a service uh with an interface as a spring bean and inject it, but the interface
would be part of the public API. That's just like another way I showed you before. Now, of course, if you're gradually um adopting um module if in your code base, you will actually like um have the Sisyphus problem because the problem is here actually like by modularizing your code and adding new features, new violations might be introduced, right? And that means like you would have to also
like rework those later and you are constantly working on a thing. So, reworking things is definitely like an ongoing discipline. Now, Spring Module if provides me different possibilities. I could use the the type open for an open module and that's quite nice during migration, right? It just helps me a bit. I could also say for example, I want the detection strategy not by default on the top
level package. I want to have it only on the packages which are annotated. So, I can easily choose the right packages to become a module, right? You could even write your own custom detection strategy if you want to. And of course, running all the time the verify I helps and even in CI you make sure that new things added to a already existing module um are not
merged if we have regressions. The next step is definitely like documentations. And documentation can be easily added because the verification step has a a representation and in-memory representation of your application. So, we just can dump into like some C4 diagrams or UML diagrams. We can also like have some canvases as well. And of course, this should actually happen in the pipeline, right? And especially like in one
single test because if you verify and then create the documentation afterwards, you know that the verification was good and then you can produce the documentation in the same test. And if the verification fails, the documents are not created, right? So, that's why you put both into the same thing. Now, besides um the diagrams, we also have a canvas where we have all the information needed like what's
the base package, what's our what are the spring beans consumed or exposed, and what are the published and consumed events, what are your configuration properties for a module, and so on and so forth. And also like what are the allowed dependencies. So, it's a very nice canvas which just contains all the information for your model contract basically. Nice documentation. If you think of like runtime, um we
can actually add a dependency Spring Module if actuator and you get an endpoint where everything is exposed via the actuator API, right? As you can see here um just like some rest and information. We just can see like what's running in production. Now, another step is definitely actually like adding observability and I love it because I need and I need to show it um because putting open
telemetry on the stack means like if you're adding the the um Spring Module if insight starter or the observability you're going to see like what's um with the traces going on between the different modules. And I'm just going to start my application here. So, that's actually like my Spring Boot application running and starting Jaeger. Um I'm going to fire a few requests. Doesn't matter like what I'm
doing here. It's just like um doing some get here um one or two and then heading over in the browser to Jaeger. So, um let's jump out here. And like let's refresh and let's see what's going on. You will actually see that we have here 12 steps in here. And you might see that we have the order I added some dependency so we see also the database
stuff and then you see like the inventory module here. So, we see that we have like this communication between the order and the inventory module. And this is like how we do microservices, right? So, um it's super nice and it's super easy to add. So, you see what's going on between the modules. Cool. So, let's talk about the second part. I said actually like we do events,
right? And these events, they're running a separate thread. But now imagine if something goes wrong, we might lose an event. By adding the Spring Module if starter JDBC, we can persist the events in the database. And it's so awesome that actually like we see like which events were um consumed but failed. So, we have to reprocess them. And that's basically published in this event publication table. So,
you could imagine now we are going to call um our uh order service. We create we create the order. That part runs in a transaction, but then actually like we publish the event in the event registry. That part runs in the same transaction. So, we we make sure that the event is persisted. But then afterwards, the event is asynchronously consumed and if this one is failed, we
can really reprocess it. So, what is really awesome about that is that um we have the possibility for example, after fixing the application and just like restarting it and you have to like turn a feature on that it works and it's coming in one of the next slides, then it will republish it automatically. There is also an API you could use and what I love to do
is for example, I use a rest endpoint where I say I want to now republish like my events, especially if you have multiple instances running of a Spring Module if application. So, that not every um instance which gets restarted is automatically like reprocessing all the events. The events can be um typically stored in in a database via JDBC, via JPA, and MongoDB. That's what's supported, but of
course, you can add your own one if you want to. this is actually like the real thing. So, you fix the thing, you redeploy basically and if you have the Spring Module if events republish outstanding events on restart equals true, it will automatically like reprocess those events which failed before, right? Or as I said, you could actually do it on the API, too. And that's that's really
awesome. So, that means like we are not losing anything, which is quite important. Another thing which also already showed this morning is actually like the scenario API for testing. And this is actually amazing because now we can test these kind of events. And with Avitility underneath, we have a super nice fluent API how we can describe our test where we wait until a certain event has happened
and then we check actually like what what the values are and so on. This is actually like super nice and we can test this like um interaction between the different There is another thing um which is quite nice because you don't always have to like start up a full-fledged Spring Boot application to your do your integration testing. Um there is this thing called application module test where
you by default only bootstrap the module and and its dependencies, its spring bean dependencies. And you can write your test against the modules. It's possible as well to say like that you want to um also load dependent modules. So, if you just like test one single module, you would have to mock out certain dependencies. If you want to test an interaction between different modules, you can start
them by adding some other uh arguments to the annotation and just name the modules you want to start with. And of course, there is the possibility to start everything if needed as well. But then you're basically there like with a Spring Boot test, right? So, that helps me actually like just to test some modules independently without starting the whole thing as an application. Of course, now we
have to go a step further because like we have modularized our application, but maybe you have the feeling that certain things need to be extracted. And because now we have events in our application, we could actually like externalize those events easily. So, that means like by adding the externalize annotation and by configuring the necessary infrastructure, we could actually like externalize events to Kafka, to RabbitMQ, or to
JMS if you want to. And that's super nice because that means like our events which are published internally can be also uh consumed externally, right? And that might be your migration path to microservices um if you need it, right? But anyhow, just because you can do it, you probably shouldn't, right? So, really like think about that twice. If you look at the picture, it actually looks like
this. So, we have now events consumed internally in our modular monolith, but also you can consume them externally um while listening to a RabbitMQ um or like a Kafka thingy, and then actually you're just like listening on the um queue like you would anyway, right? And you can take the information in the message and then just like work on that. Of course, it could be on the
other side as well again, a Spring Modulith application, but it could be something else, too. So, um there is also like some things um where you would say like, "No, we are not going further. We don't want to have like microservices." Because sometimes you really don't need microservices. And as we also know like microservices were trend for a while, now it's probably also like moving backwards, but
we say like we want to have more structure. And um you you really know probably also like how much load you have on your application and like how your users are going your application if it makes sense or not. a well-structured Modulith might be actually like the right state. And now even without paying this distributed systems tax actually like this additional complexity, right? So, um yeah. And
if your team is small and your domain fits into one deployment, why not just like shipping it fast with one single deployment and stay modular, So, one of the big architects once said, "When you need microservices, you'll know. Until then, don't split." And that's basically like the whole thing I'm saying all the time now these days, right? So, um yeah, we really need to have reasons why
to make our systems more complex. Good. So, now actually like the next step would be how would you start? And it's actually super easy by adding the bomb files to your project, by adding the starter for the core modules, a starter for tests. You have to write dependencies. You might see it already in your IDE. You can run the verification and so on and so forth. You
can easily generate documentation. You have like um already some insights how your application looks like. You're going to pick one module, you refactor it, and then actually like um you might use events to protect against direct access to have this kind of more flexibility, and of course, you repeat the whole cycle sprint by sprint and you improve your application. I know we don't have time to do
Q&A here, but I would love to talk with you about your context and how you could apply um Spring Modulith, but we can do that in the break, and I'm also around till tomorrow. So, let's do that. So, the takeaways actually for you should be modular monoliths gives you structure. That's very important. Then actually like we have conventions which are built in, and we just saw that
it's like one single test we need to write. There is not much to write like on ArchUnit and others because it's already done inside. Testing, documentation, and observability is provided, too. Makes our life way easier and things are visible. Events help us with decoupling modules and prepare for future extraction. We have to test also like in our pipeline, and you can actually like prepare for regression and
make sure that they don't go into your code. You have documentation already generated, and you can like build up on that. And then of course, the only thing what you need is a little bit of time because you can easily start small, you can add one test, you work with one module, and you probably need one afternoon to do the first experiments. It's super lightweight, and I
highly recommend that you do that. I'm sure there are also some pitfalls, and you have been there for sure because you have for sure as well some utils or common package which is just huge. So, don't go into that direction. We want to clean up things. You probably also have skipped in your project one test because it's failing. That's not the goal, right? We don't want to
fix it later. We want to have it better. Um you maybe went already the route where you said like, "We have split into microservices, and we shouldn't have done that, right?" Maybe. Maybe you move back to micro uh to not microservices, Spring Modulith. That's maybe good idea. And the other thing which I see typically is also that people come up with too many modules. So, start small,
enforce things in CI, and stay disciplined. I would encourage you because now it's your turn. Give me some feedback on the Whoop app. I shared a lot of knowledge in the last 45 minutes, and I would really appreciate your feedback, right? Good. So, that was my presentation from chaos to cohesion. Structure is a journey, and it's not a destination. You don't need permission to start the whole
thing. You don't need a rewrite, right? So, it's super simple. As we have heard before, one module at a time, one test, and one afternoon is actually good enough. Thank you. >> [applause] >> If you want to have access to the slides and the demos, you can scan the QR code, and you will find it on my GitHub repository, and um there should be a read me
to it so you can easily like just execute it on your machine.
More from this event
See all 38 talks →
Spring I/O 2026 Keynote
1:08:44
The Spring AI Ecosystem in 2026: From Foundations to Agents @ Spring I/O 2026
43:39
Breaching LLM-Powered Applications: Overcoming Security and Privacy Challenges by Brian Vermeer
48:40
New in Spring Security 7: MFA, OAuth2 and more by Daniel Garnier @ Spring I/O 2026
46:43