About this talk
This talk is presented by Sergey Chernov and focuses on the migration of legacy applications to Spring Boot. The speaker discusses the advantages of adopting Spring Boot, including better scalability, unified application platforms for logging and monitoring, and improved integration testing. He highlights the challenges inherent in migrating a large, evolving codebase, emphasizing the need to maintain regular release cycles and feature development during the transition. The session includes a step-by-step guide on migrating code, utilizing tools like Maven and artificial intelligence to enhance the testing process and ensure stability. Finally, he shares practical tips on how to incrementally convert existing servlet-based applications to Spring Boot while managing dependencies effectively.
Full transcript
[music] >> Hello. My name is Sergey Chernov, and today I'd like to talk about migrating legacy applications to Spring Boot. Uh small disclaimer at the end of this presentation, the slides uh will be shared. So, if you do have enough time to recognize some code snippets, don't worry. today we'll talk about what can be reasonable to migrate your legacy stack to modern Spring Boot, how to adopt
dependency injection to a legacy code base that is full of static uh classic get instance methods, uh and static singletons, uh how to switch from web application container to embedded server, and how to avoid interruption of regular uh release cycles and feature development. So, in other words, how to avoid rewriting your application from scratch, for example. And for sure, we need to also talk about how AI
changed this uh migration process nowadays. So, to avoid confusion, we are not talking about things which are too specific to uh Java EE stack, like Enterprise Java Beans or CDI. So, I work at Miro, and at Miro we do online whiteboards for collaboration. The company started in 2011, and this moment there was no Spring Boot. And few years later, we started adoption of Spring Boot uh 1,
and uh nowadays we have uh three Spring Boot 3.5 and huge monolith application. And also, we have around 200 microservices using Spring Boot as well. why it can be reasonable to migrate uh your applications to Spring Boot? As a platform engineer, uh you can have unify application platform, for example, for common things like logging, monitoring, uh deployments, etc. Also, applications that are based on Spring Boot uh
stack mm are better scaled. What I mean here is that mm Spring Boot itself enforces some uh good practices and proper guidelines. Uh and it scales better when you add more modules, more features, and more teams are involved uh to contribute to the same project. Also, for sure, powerful and flexible integration testing. And uh with Spring Boot, you can use embedded web server instead of uh web
uh servlet container, which can be way more convenient uh for your deployment, especially based on uh Kubernetes environments. So, what is the problem? If your project is huge and actively evolving, it can be quite challenging to find a compromise how not to interrupt your regular work and feature delivery. And also, probably you don't want to make it in a huge one change from one state to to
another, because it will also introduce risks. So, the good news that it's possible to uh accomplish in uh few steps. So, we need a sample application. So, I asked uh open source cloud to generate I application alike Spring Pet Clinic, no Spring Framework. It should uh have uh web archive assembly to war file. It should use HTTP servlets, and it should use static singletons uh defining like
and libraries only Postgres uh driver, Jackson, and Flyway. we'll have a lot of classes like this. So, here I would just want to pay attention to how usually our get instance method looked like. So, it's synchronized, creating instance of our object on demand, resolving all uh dependencies where calling their get instance. All good singletons. first thing first, uh we plan huge migration, uh which can be potentially
breaking, and we need to ensure uh that we are not introducing new problems with E2E tests. So, uh pro tip, if you're uh using uh Maven, you can use uh Cargo plugin that will start Tomcat, deploy your web application there, then you run uh E2E tests, and then it shut down the Tomcat. Uh to avoid ambiguity, when I say E2E test, I mean any test that executed
outside of JVM with your web applications. So, use any test framework you like. Use AI to generate it to generate those tests. It's quite good to generate your tests based on the source code. And it would be nice if you automate it in CI, for example, if you with plugin that is explained, you can simply run it Maven clean verify in one command. Okay, it's 2026, so
I must admit that I had to rewrite original uh slides of my presentation, and try to use uh AI. Again, in my case, it was Cloud to migrate my application to Spring Boot stack. The prompt was quite trivial. So, refactor this application to Spring Boot 4 uh with Uber JAR packaging. So, I didn't define any kind of skills. I didn't define any agents, nothing like And surprisingly,
it went quite well. So, it made only two small mistakes. First, it insisted that now I need to update JVM to 21, which is technically not correct, but still a good advice. And the second, it forgot to add one dependency to start Flyway. I think it's because uh the Spring Boot 4 rearranged those dependencies a bit. So, what it did, it migrated servlets to REST controllers. The
database layer was migrated to JPA entities and uh Spring Data Repositories. Introduced application properties. I would say it's quite good. So, for example, if you prefer another database stack, you can specify what kind of stack you want. Also, mm I tried to discuss with Opus uh how this can be decomposed to multiple steps, and their advice is quite reasonable. my advice here, be pragmatic, try AI, but
be careful, and especially take care uh E2E tests. if you decided to migrate yourself, or AI uh doesn't work for you, or maybe uh you're control freak like like me, uh let's uh discuss what steps uh can you help there. I recommend you to check uh start.spring.io and generate application in the target state how you see it. So, for example, if you want to use Spring Boot
uh and Maven uh combination, you can see which is uh actual uh standard idiomatic approach uh to do this, which dependencies should be defined, which POM files should be inherited uh or for Gradle files, which plugins should be applied. It I used this uh a lot while pre- preparing this presentation to see uh my target application should look now we need to choose Spring Boot version. So,
actually, uh the first rule is that if you're still using Java EE namespace in your application, the maximum Spring Boot version is the second. But I strongly recommend you not to adopt Spring Boot. First, because Spring Boot is already uh already does not release new open source versions. Also, uh migration from Spring Boot 2 to Spring Boot 3 will be way more complicated [clears throat] for you.
So, first, migrate from Java EE to Jakarta namespace. Uh also, ensure that your uh Java version is at least 17, ideally 25. now we have two options, like Spring Boot 3.5 and 4. Spring 3.5 is still uh released, but already in June of 2026, their end of public open source releases. Just keep in mind. So, nowadays I would recommend only one option, 4.0 or even 4.1, because
it's already milestone 4, so it will be released pretty soon. Okay. Now we need to introduce dependency management uh for Spring uh components in our project. If you check projects that are generated with uh Spring Starter, they usually inherit a super POM parent, which is named Spring Boot Starter Parent, configures build plugins and dependency management. Uh for many projects, uh uh it can be not an option,
especially if you have your own parent POMs. So, instead, you can use uh importing of the POM file, which name is a bit different, Spring Boot Dependencies. Here, I'd like to pay attention that position of this declaration makes sense. The higher it's defined in your list of dependency management, the higher priority it actually has. So, you need to find some compromise declaring your own dependencies and which
dependencies you want to inherit from the Spring Boot. So, once you add this dependency uh management entity, A of your dependencies can be changed because now they are partially managed by Spring Boot. And it defines not only Spring Boot, but, uh, and Spring Framework, but also all related technologies starting from Kafka ending like Jackson. So, what we need to do is to compare, list of uh, libraries
that are assembled to your project. So, make a war file before and after this dependency management entity is added. And compare line by line. Actually, you can see in this example, for example, that Jackson, uh, versions is updated from like 17 to 21 because those versions are now managed by Spring Boot. But also you can notice that which is represented by a group of libraries, is now
inconsistent. So, this may lead to so-called jar hell and issues during the runtime. So, check the, uh, list carefully. Also, not all libraries, including Jackson, are strictly following semantic versioning. So, even if this looks like a minor update from 217 to 221, there can be a lot of breaking changes, not only, uh, binary compatible, but a behavioral as well. Okay. So, we made our first contribution. We
added dependency management. What we do now? We build our application, run e2e test, and we will, uh, do this on each iteration. Ideally, you need you make a commit, you make a release, and ensure that your production is not broken. Uh, do not aggregate too many changes at once. Next iteration is, let's add our first Spring dependency. Dependency that we need is called Spring Boot Starter. So,
it brings, uh, basic Spring Boot components, uh, all that is needed for bootstrap, some Spring Core Framework, and also logging implementation. By default, Spring Boot is using Logback. If you prefer Log4j, you just need to exclude Logback from dependency, declaration here. And and you will use Log4j. what we do again? We change the state of our project. We need to build it, uh, run e2e test, and
ensure that everything is fine because this can be quite fragile and side effects are unpredictable even if you just updated libraries. So, ensure you go safe. Especially if your application is huge and has complicated logic. Also, check that logging is working fine because, uh, this may also affect, uh, logging configurations. Classic Spring Boot application always has this class, uh, annotated as Spring Boot application. This the entry
point. Let's add such class to our project. Uh, pay attention that it should be located in the root package of the app, uh, [snorts] because it affects so-called component scan of your application. Right now, we don't have any component, but later it will be convenient to scan, uh, nested, uh, packages. So, uh, this class should have Spring Boot application annotation and the main method, uh, which is
quite standard. Let's try to run this method. We can do it from IntelliJ IDEA. So, what we see? Uh, it prints, uh, Spring banner, uh, also some minimal, uh, log output, and then exits immediately with code zero. This is expected because in current state Spring Boot doesn't start any server, it doesn't start any thread, and this is normal that it exits immediately. Okay, this is success. Alternatively,
something can go wrong. Actually, depending on the class path of uh, Spring can discover auto configurations that are present in the class path. For example, if you have Hikari, uh, it will try to create Hikari connection pool, and it will fail, uh, simply because it will not be able to find environment and environment parameters for the connection pool. So, you need to inspect the log to figure
out which actual, um, auto configuration is failing. just pro tip, you can, uh, enable debug logging for this class with its name, and it will print pretty detailed log. This auto configurations are matched, this are not. So, you can, uh, spot which actual configuration is suspected. Uh, we figured out that it was data source auto configuration. What we need to do? We define exclusion. So, run again.
Now, our application starts and What we do now? Commit, build, release, go to next iteration. Okay, so, to introduce, uh, another, uh, entry point, uh, that will initialize our Spring Boot application. Actually, when we start our web application as a archive, it it doesn't care about the main method in Spring Boot application because, uh, the entry point actual is Tomcat itself. So, we need to introduce some
kind of helper, servlet context listener. So, what it does? Mostly the same as a main method, it's just extracted to few calls. So, it runs the Spring application, save the reference to application context, and, uh, on the context destroyed event, it shuts down the context. So, annotated to web listener or define it in your web.xml. Okay. So, what we do now? We start Tomcat. We deploy our
web application inside, uh, and in the log we can see Tomcat starting, web application starting, and inside we see uh, banner. So, we have embedded, uh, Spring Boot here. this can be quite complicated, so let's make a short break. So, we are in the intermediate state. We have embedded Spring Boot inside of web application inside of, uh, Tomcat. Our target state is opposite. So, uh, our, uh,
Spring Boot should bootstrap Tomcat itself and run the web application. But we will get get to this Next thing is introducing static accessor, uh, to a Spring application context. Normally, this is a severe anti-pattern and and the usual, uh, Spring Boot application shouldn't have it, uh, but I think most of us introduced classes like this. I did it several times. So, it has like two helper methods.
First is, uh, get bean to get bean instance by the class, and get bean with a string, uh, qualifier. This can be helpful if we have several beans of the same time in the Spring context. So, for example, uh, database connection pools, which have the same type but different names. we need to initialize this holder. And the, um, challenge here that we needed to initialize it before
Spring creates any kind of bean any kind of beans because otherwise they will they will see an uninitialized state of this class. Probably there are several ways to do this. Uh, one that I found, uh, and I think it's convenient, is to define implementation of the interface which is called application context initializer. So, it has only one method to to be initialize to be implemented that actually
receives application context as an argument. So, now we need to declare this class in a very special way, uh, inside of META-INF resources, we create a file which is named spring.factories, and this name is fixed. And with a simple, uh, content. So, uh, application context initializer equals the value which is name of our So, what is this for? Spring will discover all files which are called spring.factories
in the class path. There can be many files like this in different modules. Read this instruction, create this initializer, uh, call, uh, initialize methods, and only after this phase, uh, Spring will start creating beans. So, beans can rely on, uh, holder already initialized. Okay, uh, it's time to define our first bean, finally. Which bean to choose? We need to find a candidate with no dependencies. So, a
bean that is usually, uh, using, for example, properties as a source, uh, of, uh, their parameters. So, what you need to do? In my case, uh, this bean, uh, this class is, uh, database config, and database config had get instance method, which was implemented like something like op- a lot, uh, of properties file, fill, uh, those parameters, and return the We need to take all this logic
and move it inside of configuration class defining this as a bean. But, uh, there are callers of, uh, get instance methods of our class, which, we still preserve. And we replace original implementation that creates instance on demand to delegating to application context holder get bean. is a temporary solution that will allow us to migrate in few steps. now we declare more beans slice slice by slice um
>> [sighs] >> beans that depend on database config. instead of calling get instance like it was before, now we can receive those beans as parameters uh to the constructor of our beans. And declaration can be different. You can annotate it like as a service, you can create configuration and define it as import, or you can define bean methods, up to you. So, and again, we keep the
method get instance in that in such class which only does delegation to get been method. iteratively uh you will get rid of those get instance calls and you can uh throw away uh those methods and uh the holder this will reveal few problems in your code if they exist. So, for example, classes that mix different scopes. So, what I mean? Imagine like uh business layer entity that
contains some fields of the request or database entity. And the same class is accessing singleton. So, if you'll try to migrate this class, you will have this uh inconsistency. Um not matching those scopes. Normally, you need to rearrange those Uh so, for example, calls should be just moved outside. Or in the worst case, you can create uh bean factories that will provide those singleton instances and create
uh instances of such classes not via the constructor, but with those factory helper methods. But, I don't recommend to do this way. Another problem is possible cyclic uh dependencies. So, if you have two singletons referencing to each other, it can be not a problem when you have static singletons, but once you will uh try to migrate this to Spring, you will have so-called cyclic dependencies problem. So,
Spring will fail to initialize this. Again, normally, you need to rearrange those methods to avoid the cyclic dependencies. But, if if it's not possible for some reason, the you in the very worst case, you have lazy. This is helper uh keyword uh which will actually create a runtime proxy um delegating to a call. Spring allows cyclic dependency between bean, but this is considered as a antipattern and
bad practice. Okay. So, we migrated already several beans. Now, we need to migrate our servlet stack. uh first, you you need uh to use annotations instead of web.xml. So, uh annotations in Jakarta stack are for example at web servlet, at uh web filter. Spring Boot 4 suggests pretty convenient uh new annotation which is called servlet component scan. So, how it works? It will uh scan all your
class path finding web servlets, web uh filters, web listeners, and other classes and register them to your Spring context. So, if you run Spring Boot application, it will register them automatically. Before Spring Boot uh 4, you could use uh so-called servlet registration bean. let's look inside our war file. So, what is a regular web archive uh war? This is a zip which has uh web inf classes
of your application and lib with the libraries. When you deploy your application, it reads descriptors, executes classes, and loads libraries. Spring Boot introduces extension for this uh archive. It's called Uber war. So, uh how it differs? It adds another directory which is called lib provided. And also uh here in the root, you can see custom launcher. So, what's the trick here? provide such uh war file, I
mean deploy to a Tomcat, it will ignore this launcher, lib provided, and load it as a regular web application as usual. But, if you run Pet Clinic as a Java application, I mean Java jar Pet Clinic war, uh check meta inf that declares main class from this loader. This main class is aware that here in lib provided, we have Tomcat. It will load Tomcat and deploy our
application on this. So, this can be a very convenient intermediate state of our web archive. Uh as you can run it as a standalone or uh deployed to web container. So, few things I'd like to pay attention here. So, you need to call repackage goal in Maven pom file. Also, you need to add new dependencies. And dependencies related to Tomcat uh defined in provided scope. Because provided
scope is added to this lib provided separate directory. Because otherwise, those Tomcat dependencies will become a part of lib and Tomcat will be able to discover own sources which is a bit weird. okay. we uh do Maven build, we run E2E, everything is fine. If we run Tomcat and our web application is starting, uh it's okay. But, let's get back to our uh application main method. Uh
let's try to run it. And what we can see, surprisingly, it gives infinite cycle printing Spring Boot banner, Spring Boot banner, Spring Boot banner. It it cannot uh initialize. Finally, it fails without of memory. So, any guess why it happens Maybe it's cycling the beans. No. Initialization calls are not working. Correct. Correct. We have a listener which is discovered by Spring Boot and this listener start nested.
Yes. Thanks. we need to disable this logic. We need to keep it in case if your application started from the Tomcat. Uh I believe it can be done in a less ugly way. So, this is the first solution that came to my mind. If we run main method, we just uh trigger this disabled and this logic is skipped. So, now we are able to run our application
as a standalone. So, we can uh run our war war file as we can deploy to Tomcat, but also we can run it as a standalone. Java jar uh Pet Clinic war. So, it will start Spring Boot, it will uh Tomcat, and our application inside. So, when we run it, we need to specify Pet Clinic as a context path. Why? Because by default, Spring Boot starts their
applications in the empty web context, while our integration tests, I mean E2E tests, are expecting this uh Pet Clinic path. So, again, running uh our web application as a standalone, run uh E2E tests, and all tests are green. So, congratulations. We now are in the There are still leftovers. So, for example, not all get instance methods are migrated. This is not a blocker in case if you
are uh migrated all entry points like HTTP servlets, uh maybe some others. So, you can You migrate so-called I don't know, let's call it root beans or entry point beans. And if they are already uh working in Spring Boot, everything else will be resolved in a way. But, eventually, you need to get rid of uh those uh old-fashioned get static uh get uh instance static methods from
the holder. So, you don't need to keep the static uh reference to your Later, you can migrate controllers to rest uh migrate your servlets to rest and uh leverage another nice Spring features. uh few conclusions here. So, yes, this is doable. You don't need to create an immense change. Uh you can reduce your risk. Make implement E2E tests. I mean, if you're lazy to do this, ask
AI. It can do it uh for you. Um try to migrate with AI. It can. Again, I was impressed how good this code. Maybe later, some other uh build tools uh AI tools can do it even better, but this looks very promising. Here you can find slides of this presentation. Uh on on the right, there is a demo project called Pet Clinic Legacy, and there are two
pull requests. One is generated with AI and second uh is following the same actually instruction uh as explained. That's it. Thank you. >> [applause]
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