Your Spring Boot app won’t start, and the stack trace says something about “ambiguous handler methods.” You haven’t changed the controller logic — only added a new endpoint — but now nothing works. This error is almost always a mapping collision, and it’s easier to fix than it looks.

Quick Answer: Spring throws IllegalStateException: Ambiguous handler methods mapped for '...' when two or more controller methods are registered for the same URL path and HTTP method. Find the duplicates using the full error message (it names both methods), remove or differentiate one of them, and restart.

What the Error Looks Like

The stack trace typically appears at startup and looks like this:

java.lang.IllegalStateException: Ambiguous handler methods mapped for '/api/users':
{public org.springframework.http.ResponseEntity
    com.example.controller.UserController.getUsers(),
 public org.springframework.http.ResponseEntity
    com.example.controller.UserController.fetchUsers()}
	at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping
	    .assertUniqueMethodMapping(AbstractHandlerMethodMapping.java:567)
	at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping
	    .detectHandlerMethods(AbstractHandlerMethodMapping.java:306)

The error message is actually very informative: Spring tells you the conflicting path (/api/users) and both methods involved. Use that information to go directly to the source of the conflict.


Step 1: Read the Error Carefully

Before anything else, copy the full exception text from your logs. The key line looks like:

Ambiguous handler methods mapped for '/api/users':
{public ... UserController.getUsers(),
 public ... UserController.fetchUsers()}

Write down:

  • The conflicting path (/api/users in this example)
  • Both fully-qualified method names
  • Which controller class each method belongs to

If both methods are in the same class, you have an internal duplication. If they’re in different classes, you have a cross-controller collision.


Cause #1: Duplicate Methods in the Same Controller

The most common cause — especially after a refactoring session or a copy-paste — is two methods in the same controller that map to identical paths with the same HTTP verb.

The problematic code:

@RestController
@RequestMapping("/api/users")
public class UserController {

    // Original method
    @GetMapping
    public List<User> getUsers() {
        return userService.findAll();
    }

    // Added later — but forgot the first one still exists
    @GetMapping
    public ResponseEntity<List<User>> fetchUsers() {
        return ResponseEntity.ok(userService.findAll());
    }
}

Both getUsers() and fetchUsers() map to GET /api/users. Spring doesn’t know which one to invoke, so it fails at startup rather than silently picking one.

The fix:

Either remove the duplicate, or give it a distinct path:

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping
    public ResponseEntity<List<User>> getUsers() {
        return ResponseEntity.ok(userService.findAll());
    }

    // Moved to a distinct sub-path if you actually need both
    @GetMapping("/list")
    public List<User> fetchUsers() {
        return userService.findAll();
    }
}

If you genuinely don’t need both methods, just delete the one you added by mistake. Don’t try to keep both with different return types — Spring’s handler mapping doesn’t differentiate on return type or response format by default.


Cause #2: Subclass Inheriting and Re-declaring a Mapping

This one is subtler. If you extend a controller class and add a method that matches one already defined in the parent, Spring registers both during context startup.

// Parent controller
@RestController
@RequestMapping("/api")
public class BaseController {

    @GetMapping("/health")
    public String health() {
        return "OK";
    }
}

// Child controller — extends the parent
@RestController
public class AppController extends BaseController {

    // Oops — same mapping as the parent
    @GetMapping("/api/health")
    public String checkHealth() {
        return "Service is up";
    }
}

Because AppController extends BaseController, it inherits the /api/health mapping. Then it adds another one explicitly. Spring sees two registered handlers for the same path.

The fix:

Don’t extend concrete controller classes in Spring unless you know exactly what you’re doing with the inherited mappings. Either override the parent method without adding a new @GetMapping annotation, or refactor the shared logic into a service:

// Option A: Override without re-annotating
@RestController
public class AppController extends BaseController {

    @Override
    public String health() {
        return "Service is up"; // No @GetMapping here — inherits parent's mapping
    }
}

// Option B: No inheritance, extract logic to a service
@RestController
@RequestMapping("/api")
public class AppController {

    private final HealthService healthService;

    @GetMapping("/health")
    public String health() {
        return healthService.check();
    }
}

Option B is almost always the better design. Controller inheritance tends to create more confusion than it solves.


Cause #3: Two Controllers With Overlapping Paths

When you have multiple controllers in your application, it’s easy for their combined class-level and method-level mappings to collide.

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping
    public List<User> getAll() {
        return userService.findAll();
    }
}

@RestController
@RequestMapping("/api")
public class AdminController {

    @GetMapping("/users")
    public List<User> listAllUsers() {
        return userService.findAll();
    }
}

UserController maps to GET /api/users (class-level /api/users + method-level @GetMapping with no path). AdminController maps to GET /api/users (class-level /api + method-level /users). They resolve to the same path.

The fix:

Make the paths genuinely distinct:

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping
    public List<User> getAll() {
        return userService.findAll();
    }
}

@RestController
@RequestMapping("/api/admin")
public class AdminController {

    @GetMapping("/users")
    public List<User> listAllUsers() {
        // Admin-specific view — different path, different authorization
        return userService.findAllWithAuditInfo();
    }
}

If both endpoints genuinely need to return the same data at the same path, consolidate them into one controller. Having two controllers respond to the same URL rarely makes sense architecturally.


Cause #4: Method Overloading With the Same Mapping

Java allows method overloading — same method name, different parameters. Developers sometimes think Spring treats overloaded methods as distinct handlers. It doesn’t.

@RestController
@RequestMapping("/api/search")
public class SearchController {

    @GetMapping
    public List<Result> search(@RequestParam String query) {
        return searchService.search(query);
    }

    // Overload — different parameter, but same HTTP method and path
    @GetMapping
    public List<Result> search(@RequestParam String query,
                               @RequestParam int limit) {
        return searchService.search(query, limit);
    }
}

Both methods map to GET /api/search. Spring’s dispatcher doesn’t consider Java method signatures when routing — it only looks at HTTP method, path, headers, and content type.

The fix:

Use optional parameters with a default value instead of overloading:

@RestController
@RequestMapping("/api/search")
public class SearchController {

    @GetMapping
    public List<Result> search(
            @RequestParam String query,
            @RequestParam(defaultValue = "20") int limit) {
        return searchService.search(query, limit);
    }
}

If the two variants really do need separate implementations, put them on separate paths (/api/search and /api/search/limited) or differentiate them by params:

// Differentiate by presence of a parameter (less common, but valid)
@GetMapping(params = "!limit")
public List<Result> searchBasic(@RequestParam String query) { ... }

@GetMapping(params = "limit")
public List<Result> searchWithLimit(@RequestParam String query,
                                    @RequestParam int limit) { ... }

The params narrowing tells Spring to route based on whether the limit parameter is present in the request.


Cause #5: Conflicting produces or consumes Without Other Differentiation

You might try to distinguish two methods purely by the produces attribute while keeping the same path:

@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public User getUser() { ... }

@GetMapping(produces = MediaType.APPLICATION_XML_VALUE)
public User getUserXml() { ... }

In some Spring versions this works cleanly; in others (especially when a client sends Accept: */*), Spring can’t determine which handler to use and throws the ambiguous mapping exception at runtime, not startup.

The fix:

Differentiate by path instead of solely by content type, or use RequestMappingHandlerMapping configuration to enforce strict content negotiation. Path differentiation is simpler and more reliable:

@GetMapping("/json")
public User getUserJson() { ... }

@GetMapping("/xml")
public User getUserXml() { ... }

Step-by-Step Diagnosis Checklist

If you’re staring at the error and not sure which cause applies, work through this checklist:

  1. Read the full error message — identify the conflicting path and both method signatures
  2. Search your codebase for the path string — grep for the literal URL segment (/api/users) across all @RequestMapping, @GetMapping, @PostMapping, etc. annotations
  3. Check class-level mappings — a class with @RequestMapping("/api") combined with a method @GetMapping("/users") produces /api/users even if neither annotation says that explicitly
  4. Look for inheritance — if any controller extends another, check whether the child re-declares inherited mappings
  5. Check overloaded methods — search for the same method name within the controller flagged in the error
  6. Look at imported configurations — if you’re using @Import or third-party starters, they may register controllers you don’t own

Use Debugly’s trace formatter to parse the full stack trace and highlight the exact IllegalStateException line — it’s especially useful when the startup log is hundreds of lines long and the relevant exception is buried.


Still Not Working?

A few less common but real scenarios:

Kotlin data classes and Spring MVC: If you’re mixing Kotlin and Java in a Spring Boot app, Kotlin’s generated component*() methods on data classes can occasionally interfere with method scanning. Make sure your @ComponentScan excludes packages it shouldn’t be scanning.

Two beans of the same controller type: If your application context somehow registers the same controller class twice (e.g., via both @ComponentScan and an explicit @Bean declaration), every mapping in that controller appears twice. Check your configuration classes for accidental double-registration.

Spring Boot DevTools and hot reload: In rare cases, DevTools can restart the context in a state where cached handler registrations collide with freshly-scanned ones. A full clean rebuild (./mvnw clean package or ./gradlew clean build) usually resolves this.

URL pattern normalization: /api/users and /api/users/ are treated as the same path in Spring MVC by default (setUseTrailingSlashMatch was true until Spring 6.x). If you’re on Spring 6 or Spring Boot 3, trailing slash matching is disabled — so these two are now distinct paths, which might actually resolve an ambiguity you had before.


Summary Checklist

Before declaring the issue fixed:

  • [ ] Removed or re-pathed the duplicate controller method
  • [ ] Verified no parent class has an inherited @RequestMapping that conflicts
  • [ ] Confirmed no two controllers combine class + method mappings to the same final URL
  • [ ] Replaced overloaded methods with a single method using optional @RequestParam defaults
  • [ ] Restarted the application and confirmed it starts without IllegalStateException
  • [ ] Ran a quick smoke test against the affected endpoints

The ambiguous handler mapping error is one of Spring’s more helpful startup failures — it tells you exactly what’s wrong and where. Once you understand how class-level and method-level mappings combine, it gets a lot easier to spot potential conflicts before they reach the startup log.

For related Spring errors, check out the posts on Spring BeanCreationException and Spring NoSuchBeanDefinitionException — both are common startup failures that often appear alongside mapping issues in complex configurations.

Use Debugly’s trace formatter to quickly parse and analyze Java stack traces — paste your full startup log and get a clean, scannable view of every exception in the chain.