Oracle WebLogic still runs a lot of serious software. Banks, insurers, logistics companies. The apps work. That’s not why teams leave.

They leave because the platform taxes everything around the code. License audits. Ten-minute deploy cycles. A shrinking pool of engineers who know what a WorkManager is. And a packaging model, WARs and EARs dropped into a shared container, that fights every modern deployment practice you want to adopt.

Spring Boot flips that model. The app owns its server instead of the server owning the app. One executable jar, starts in seconds, runs the same on a laptop and in a container.

The migration itself is very doable. Most WebLogic concepts have a direct Spring Boot equivalent, and I’ll map them all below. But a couple of things, distributed XA transactions in particular, do not translate cleanly, and pretending otherwise is how these projects go sideways.

Here’s the full roadmap: what maps where, what doesn’t, and how to sequence the work so you’re never stuck halfway.

Why Teams Actually Leave

The trigger is usually one of these:

  • Licensing pressure. WebLogic licensing is priced for a world where one app server ran everything. Renewal season and an Oracle audit letter have kicked off more migrations than any technical argument.
  • Deploy speed. Restarting a managed server that hosts six EARs to ship one change is a coordination problem, not an engineering problem. Teams that want daily deploys can’t get there.
  • Ops overhead. Node managers, admin servers, domain configuration, patching cycles. Somebody has to own all of that, and that somebody is expensive and hard to replace.
  • Hiring. Nobody coming out of the last decade of Java work knows WebLogic. Everyone knows Spring Boot. Every year on the app server makes the team harder to staff.
  • Container readiness. You can put WebLogic in a container. It’s miserable. Spring Boot was built for it.

Notice none of these are “the app is bad.” The app is usually fine. That matters for strategy: this is a re-platforming, not a rewrite, and you should resist every urge to make it a rewrite.

WebLogic domain Admin Server Node Manager Managed Server 1 orders.ear billing.ear Managed Server 2 reports.ear legacy.war shared JVMs, one deploy pipeline, domain config Spring Boot orders-service.jar embedded Tomcat, own JVM billing-service.jar embedded Tomcat, own JVM reports-service.jar embedded Tomcat, own JVM independent deploys, container-ready
The shape of the move: applications stop sharing a container and each one owns its runtime.

What Maps Where

Most of a WebLogic application translates directly. Here’s the map, then the details.

WebLogic conceptSpring Boot equivalent
Stateless session bean (@Stateless)@Service with constructor injection
Stateful session bean (@Stateful)No direct equivalent; externalize state (DB, Redis)
Message-driven bean (MDB)@JmsListener or @KafkaListener
JNDI datasourcespring.datasource.* properties + HikariCP
web.xml servlet mappings@RestController + autoconfiguration
weblogic.xml / weblogic-application.xmlProperties, or nothing at all
JTA/XA distributed transactionsSingle-resource @Transactional, outbox pattern, or standalone JTA
Security realmsSpring Security
EJB Timer Service / WorkManager@Scheduled, @Async, or Quartz
Clustering + in-memory session replicationStateless services + Spring Session with Redis
T3 / RMI clientsREST or messaging

EJBs Become Spring Beans

Stateless session beans are the easy win. The container-managed lifecycle, pooling, and injection all have Spring equivalents that are simpler than what they replace.

Before, on WebLogic:

@Stateless
public class OrderService {

    @EJB
    private InventoryService inventoryService;

    @Resource(mappedName = "jdbc/OrdersDS")
    private DataSource dataSource;

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public OrderConfirmation placeOrder(OrderRequest request) {
        inventoryService.reserve(request.getItems());
        // persist order via dataSource
        return new OrderConfirmation(request.getOrderId());
    }
}

After, in Spring Boot:

@Service
public class OrderService {

    private final InventoryService inventoryService;
    private final OrderRepository orderRepository;

    public OrderService(InventoryService inventoryService,
                        OrderRepository orderRepository) {
        this.inventoryService = inventoryService;
        this.orderRepository = orderRepository;
    }

    @Transactional
    public OrderConfirmation placeOrder(OrderRequest request) {
        inventoryService.reserve(request.getItems());
        orderRepository.save(Order.from(request));
        return new OrderConfirmation(request.getOrderId());
    }
}

Constructor injection instead of @EJB. @Transactional instead of @TransactionAttribute. A repository instead of raw datasource plumbing. The business logic doesn’t change, which is exactly what you want.

Stateful session beans are a different story. Spring’s default scope is singleton, and while prototype and session scopes exist, the honest answer is that conversational state held in server memory is the pattern you’re leaving behind. Move that state into the database or a cache like Redis. It’s a redesign, so find your @Stateful beans early. Most codebases have far fewer than they think.

MDBs Become Listeners

Message-driven beans map almost one to one.

Before:

@MessageDriven(activationConfig = {
    @ActivationConfigProperty(propertyName = "destinationJndiName",
                              propertyValue = "jms/OrderQueue")
})
public class OrderListener implements MessageListener {
    public void onMessage(Message message) {
        // unwrap TextMessage, parse, process
    }
}

After:

@Component
public class OrderListener {

    private final OrderService orderService;

    public OrderListener(OrderService orderService) {
        this.orderService = orderService;
    }

    @JmsListener(destination = "order-queue")
    public void onOrder(OrderRequest request) {
        orderService.placeOrder(request);
    }
}

Spring handles message conversion, so you get a typed payload instead of unwrapping TextMessage by hand. If you were on WebLogic JMS, this is also the moment to decide whether the destination moves to ActiveMQ Artemis, or whether the use case is really an event stream that belongs on Kafka with @KafkaListener. Don’t keep WebLogic around just to be a JMS broker.

JNDI Datasources Become Properties

On WebLogic, the datasource lives in the domain configuration and the app looks it up:

Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("jdbc/OrdersDS");

In Spring Boot, it’s configuration shipped with the app:

spring:
  datasource:
    url: jdbc:postgresql://db.internal:5432/orders
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      connection-timeout: 30000

Autoconfiguration builds a HikariCP pool from this. Credentials come from environment variables or a secrets manager, not a domain config only the WebLogic admin can touch. This one change removes an entire class of “works in QA, broken in prod” tickets, because the app’s configuration finally travels with the app.

Deployment Descriptors Mostly Disappear

A typical web.xml servlet mapping:

<servlet>
    <servlet-name>OrderServlet</servlet-name>
    <servlet-class>com.example.OrderServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>OrderServlet</servlet-name>
    <url-pattern>/orders/*</url-pattern>
</servlet-mapping>

Becomes:

@RestController
@RequestMapping("/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public OrderConfirmation create(@RequestBody OrderRequest request) {
        return orderService.placeOrder(request);
    }
}

The vendor descriptors, weblogic.xml and weblogic-application.xml, need a line-by-line audit rather than a translation. Some entries map to Spring Boot properties (context roots, session timeouts). Some map to nothing because the problem no longer exists (classloader filtering, library refs). And some encode real behavior your app silently depends on, which is why the audit matters. More on that in the risks section.

JTA and XA: The Hard Part

Here’s the honest section.

If your code does this inside one container-managed transaction:

  1. Write to an Oracle database
  2. Publish to a JMS queue
  3. Write to a second database

then WebLogic’s transaction manager has been running two-phase commit across all three resources, invisibly, for years. Spring Boot does not ship a distributed transaction manager. Plain @Transactional coordinates exactly one resource.

You have three options, in order of preference:

  1. Narrow to single-resource transactions. Most “distributed” transactions turn out to be one database plus a message send. Restructure so the DB commit is the source of truth.
  2. Use the transactional outbox pattern. Write the outgoing message to an outbox table in the same local transaction as the business data, then relay it to the broker afterward. You trade atomic-across-resources for at-least-once delivery plus idempotent consumers. That trade is almost always correct.
  3. Bring in a standalone JTA manager like Narayana. It works, but you’ve reimported the complexity you were escaping. Treat it as a last resort for flows you genuinely cannot redesign yet.

Budget real design time here. Every other section in this article is translation. This one is architecture.

The Rest of the Map

WebLogic-specific APIs and T3. Grep for weblogic.* imports; each one is a work item. T3 clients, other systems calling your EJBs over WebLogic’s RMI protocol, are a coupling problem: those callers need a REST endpoint or a queue to move to, and they need it before you can turn the server off. Find them in week one, because their owners have their own release schedules.

Security realms become Spring Security. Container-managed auth, realm config, and <security-constraint> blocks become a SecurityFilterChain bean. If the realm fronts LDAP or Active Directory, Spring Security speaks to those directly. This is well-trodden ground, just don’t leave it for last, since auth touches everything.

Timers and WorkManagers. EJB timers become @Scheduled methods. If you need persistence, clustering, or misfire handling, use Quartz with its JDBC job store. WorkManager usage becomes @Async with an explicitly configured thread pool. Do not skip the pool configuration: WebLogic tuned those threads for you, and the Spring defaults won’t match.

Clustering and session replication. WebLogic clusters replicate HTTP sessions across nodes so any server can take any request. The Spring Boot answer is to stop needing that: keep services stateless and scale by adding instances behind a plain load balancer. For the session state you can’t eliminate, Spring Session with Redis moves it to an external store, and it also covers the stateful-bean refugees from earlier.

The Phased Strategy

Do not big-bang this. The sequencing below keeps a shippable system at every step.

Phase 1: Inventory. Before any code moves, build the list: every EAR and WAR, every EJB by type, every JNDI name, every queue, every weblogic.* import, every T3 client, and every transaction that spans two resources. This takes days, not weeks, and it converts “we should get off WebLogic” into a scoped plan with a defensible order of attack.

Phase 2: Strangler fig, one app at a time. Pick the app with the fewest container dependencies, not the most important one, and move it end to end. You’ll surface the surprises (there are always surprises) on the app where they’re cheapest. Each subsequent app benefits from the patterns the first one established.

Phase 3: Keep the WAR deployable as a bridge. Spring Boot supports WAR packaging. Extend SpringBootServletInitializer, mark the embedded Tomcat dependency as provided, and the converted app deploys to WebLogic like it always has:

@SpringBootApplication
public class OrdersApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(OrdersApplication.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(OrdersApplication.class, args);
    }
}

This decouples the code migration from the infrastructure migration. Ops keeps its known deployment process while the internals move from EJB to Spring, and each release stays small.

Phase 4: Flip to embedded. Once an app has no container dependencies left, switch the packaging to an executable jar, deploy it as its own process (or container), and route traffic over. Repeat per app. When the last one flips, decommission the domain, and the license conversation at the next renewal gets a lot shorter.

Where This Bites

Three risks show up in nearly every one of these projects.

Hidden classloader behavior. WebLogic’s classloading hierarchy, with its prefer-web-inf-classes and prefer-application-packages filtering, may be quietly resolving version conflicts your dependency tree doesn’t know it has. On Spring Boot’s flat classpath, those conflicts surface as runtime errors, often as NoSuchMethodError in a code path nobody tested. Run a full dependency-tree audit early and pin the versions you actually intend to use.

Container-managed transaction assumptions. Code written under CMT assumes an ambient transaction is always there. Helper methods that “just work” on WebLogic can silently run without a transaction in Spring if a @Transactional boundary sits in the wrong place, and self-invocation, one method calling another @Transactional method on the same class, bypasses Spring’s proxy entirely. These bugs don’t throw. They corrupt data quietly. Integration tests that assert rollback behavior are your defense.

Vendor descriptor magic. That weblogic.xml entry someone added in 2013 might be capping a thread pool, pinning session cookie behavior, or ordering resource references in a way the app depends on. Nothing translates it automatically, and its absence fails silently. Audit every line of every vendor descriptor and either map it to explicit configuration or document why it’s dead.

None of these should stop the migration. All of them will cost you an unplanned month if you find them in production instead of in the audit.

When to Get Help

If you have one WAR, a handful of stateless beans, and no XA, a strong team can run this playbook without outside help. The mappings above are most of what you need.

The projects that go wrong share a profile: multiple EARs with shared libraries, XA transactions nobody fully understands, T3 clients owned by other teams, and a decade of descriptor entries with no surviving author. On those, the code translation is maybe a third of the work. The rest is sequencing, transaction redesign, and knowing which surprises are routine versus which ones sink timelines.

We do this work regularly. If you’re staring at a WebLogic estate and trying to figure out whether this is a quarter or a multi-year program, reach out and we can walk through your specific situation.

Frequently Asked Questions

How long does a WebLogic to Spring Boot migration take?

It depends on how deep the app server integration goes. A single WAR with a handful of stateless session beans and JNDI datasources can move in weeks. A portfolio of EAR files with XA transactions, WorkManagers, and T3 clients between them is a multi-quarter program. The inventory phase exists precisely to answer this question before anyone commits to a date.

Do I have to rewrite my EJBs to move to Spring Boot?

Stateless session beans translate almost mechanically to Spring services with constructor injection, and message-driven beans map to JMS or Kafka listeners. Stateful session beans are the exception. Spring has no direct equivalent, so that state usually moves to a database or external cache, which is a redesign, not a translation.

Can Spring Boot still deploy as a WAR to WebLogic during the transition?

Yes. Spring Boot supports WAR packaging with a SpringBootServletInitializer, so a converted application can keep running on WebLogic while you migrate the internals. This is the standard bridge strategy. Once the code no longer depends on container services, you flip the packaging to an executable jar with an embedded server.

What is the hardest part of leaving WebLogic?

Distributed XA transactions. If your code spans a database and a JMS broker in one atomic transaction, WebLogic’s transaction manager has been doing invisible work for you. Replicating that in Spring Boot means either bringing in a standalone JTA manager or redesigning those flows around single-resource transactions and patterns like the transactional outbox. Everything else on the migration is more mechanical than this.

Java Modernization Readiness Assessment

15 questions your team should answer before starting a migration. Takes 10 minutes. Could save you months.