diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
index 41a9d39bf3fed..698225dd0f3a3 100644
--- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
+++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
@@ -68,6 +68,8 @@ It is an exact path match because it does not end with `*`.
 <3> This permission set references the previously defined policy.
 `roles1` is an example name; you can call the permission sets whatever you want.
 
+WARNING: The `/forbidden` exact path in the example above will not secure the `/forbidden/` path. Don't forget to add new exact path for the `/forbidden/` path.
+
 === Custom HttpSecurityPolicy
 
 Sometimes it might be useful to register your own named policy. You can get it done by creating application scoped CDI
@@ -123,10 +125,12 @@ Otherwise, it queries for an exact match and only matches that specific path:
 
 [source,properties]
 ----
-quarkus.http.auth.permission.permit1.paths=/public/*,/css/*,/js/*,/robots.txt
+quarkus.http.auth.permission.permit1.paths=/public*,/css/*,/js/*,/robots.txt    <1>
 quarkus.http.auth.permission.permit1.policy=permit
 quarkus.http.auth.permission.permit1.methods=GET,HEAD
 ----
+<1> The `$$*$$` wildcard at the end of the path matches zero or more path segments, but never any word starting from the `/public` path.
+For that reason, a path like `/public-info` is not matched by this pattern.
 
 === Matching a path but not a method
 
@@ -170,6 +174,59 @@ quarkus.http.auth.permission.public.policy=permit
 ----
 ====
 
+=== Matching multiple sub-paths: longest path to the `*` wildcard wins
+
+Previous examples shown how you can match all sub-paths when a path ends with the `$$*$$` wildcard.
+The `$$*$$` wildcard can also be used in the middle of the path, in which case it represents exactly one path segment.
+You can't combine this wildcard with any other path segment character, therefore the `$$*$$` wildcard will always be
+enclosed with path separators as in the `/public/$$*$$/about-us` path.
+
+What happens if multiple path patterns matches same request path?
+Matching is always done on the "longest sub-path to the `$$*$$` wildcard wins" basis.
+Every path segment character is considered more specific than the `$$*$$` wildcard.
+
+Here is a simple example:
+
+[source,properties]
+----
+quarkus.http.auth.permission.secured.paths=/api/*/detail                    <1>
+quarkus.http.auth.permission.secured.policy=authenticated
+quarkus.http.auth.permission.public.paths=/api/public-product/detail        <2>
+quarkus.http.auth.permission.public.policy=permit
+----
+<1> Request paths like `/api/product/detail` can only be accessed by authenticated users.
+<2> The path `/api/public-product/detail` is more specific, therefore accessible by anyone.
+
+[IMPORTANT]
+====
+All paths secured with the authorization using configuration should be tested.
+Writing path patterns with multiple wildcards can be cumbersome.
+Please make sure paths are authorized as you intended.
+====
+
+In the following example, paths are ordered from the most specific to the least specific one:
+
+.Request path `/one/two/three/four/five` matches ordered from the most specific to the least specific path
+
+[source, text]
+----
+/one/two/three/four/five
+/one/two/three/four/*
+/one/two/three/*/five
+/one/two/three/*/*
+/one/two/*/four/five
+/one/*/three/four/five
+/*/two/three/four/five
+/*/two/three/*/five
+/*
+----
+
+[IMPORTANT]
+====
+The `$$*$$` wildcard at the end of the path matches zero or more path segments.
+The `$$*$$` wildcard placed anywhere else matches exactly one path segment.
+====
+
 === Matching multiple paths: most specific method wins
 
 When a path is registered with multiple permission sets, the permission sets explicitly specifying an HTTP method that matches the request take precedence.
diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java
index 08679d345bcaa..3169e6bb6067d 100644
--- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java
+++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java
@@ -41,6 +41,18 @@ public class PathMatchingHttpSecurityPolicyTest {
             "quarkus.http.auth.permission.public.policy=permit\n" +
             "quarkus.http.auth.permission.foo.paths=/api/foo/bar\n" +
             "quarkus.http.auth.permission.foo.policy=authenticated\n" +
+            "quarkus.http.auth.permission.inner-wildcard.paths=/api/*/bar\n" +
+            "quarkus.http.auth.permission.inner-wildcard.policy=authenticated\n" +
+            "quarkus.http.auth.permission.inner-wildcard2.paths=/api/next/*/prev\n" +
+            "quarkus.http.auth.permission.inner-wildcard2.policy=authenticated\n" +
+            "quarkus.http.auth.permission.inner-wildcard3.paths=/api/one/*/three/*\n" +
+            "quarkus.http.auth.permission.inner-wildcard3.policy=authenticated\n" +
+            "quarkus.http.auth.permission.inner-wildcard4.paths=/api/one/*/*/five\n" +
+            "quarkus.http.auth.permission.inner-wildcard4.policy=authenticated\n" +
+            "quarkus.http.auth.permission.inner-wildcard5.paths=/api/one/*/jamaica/*\n" +
+            "quarkus.http.auth.permission.inner-wildcard5.policy=permit\n" +
+            "quarkus.http.auth.permission.inner-wildcard6.paths=/api/*/sadly/*/dont-know\n" +
+            "quarkus.http.auth.permission.inner-wildcard6.policy=deny\n" +
             "quarkus.http.auth.permission.baz.paths=/api/baz\n" +
             "quarkus.http.auth.permission.baz.policy=authenticated\n" +
             "quarkus.http.auth.permission.static-resource.paths=/static-file.html\n" +
@@ -85,6 +97,25 @@ private WebClient getClient() {
         return client;
     }
 
+    @Test
+    public void testInnerWildcardPath() {
+        assurePath("/api/any-value/bar", 401);
+        assurePath("/api/any-value/bar", 401);
+        assurePath("/api/next/any-value/prev", 401);
+        assurePath("/api/one/two/three/four", 401);
+        assurePath("/api////any-value//////bar", 401);
+        assurePath("/api/next///////any-value////prev", 401);
+        assurePath("////api//one/two//three////four?door=wood", 401);
+        assurePath("/api/one/three/four/five", 401);
+        assurePath("/api/one/3/4/five", 401);
+        assurePath("////api/one///3/4/five", 401);
+        assurePath("/api/now/sadly/i/dont-know", 401);
+        assurePath("/api/now/sadly///i/dont-know", 401);
+        assurePath("/api/one/three/jamaica/five", 200);
+        assurePath("/api/one/three/jamaica/football", 200);
+        assurePath("/api/now/sally/i/dont-know", 200);
+    }
+
     @ParameterizedTest
     @ValueSource(strings = {
             // path policy without wildcard
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java
index 304688d56f51d..3371e6c365162 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java
@@ -23,6 +23,7 @@
 import io.quarkus.vertx.http.runtime.PolicyMappingConfig;
 import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.AuthorizationRequestContext;
 import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.CheckResult;
+import io.quarkus.vertx.http.runtime.security.ImmutablePathMatcher.PathMatch;
 import io.smallrye.mutiny.Uni;
 import io.vertx.ext.web.RoutingContext;
 
@@ -33,15 +34,15 @@
  */
 public class AbstractPathMatchingHttpSecurityPolicy {
 
-    private final PathMatcher<List<HttpMatcher>> pathMatcher = new PathMatcher<>();
+    private final ImmutablePathMatcher<List<HttpMatcher>> pathMatcher;
 
     AbstractPathMatchingHttpSecurityPolicy(Map<String, PolicyMappingConfig> permissions,
             Map<String, PolicyConfig> rolePolicy, String rootPath, Instance<HttpSecurityPolicy> installedPolicies) {
-        init(permissions, toNamedHttpSecPolicies(rolePolicy, installedPolicies), rootPath);
+        pathMatcher = init(permissions, toNamedHttpSecPolicies(rolePolicy, installedPolicies), rootPath);
     }
 
     public String getAuthMechanismName(RoutingContext routingContext) {
-        PathMatcher.PathMatch<List<HttpMatcher>> toCheck = pathMatcher.match(routingContext.normalizedPath());
+        PathMatch<List<HttpMatcher>> toCheck = pathMatcher.match(routingContext.normalizedPath());
         if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) {
             return null;
         }
@@ -93,9 +94,9 @@ public Uni<? extends CheckResult> apply(CheckResult checkResult) {
                 });
     }
 
-    private void init(Map<String, PolicyMappingConfig> permissions,
+    private static ImmutablePathMatcher<List<HttpMatcher>> init(Map<String, PolicyMappingConfig> permissions,
             Map<String, HttpSecurityPolicy> permissionCheckers, String rootPath) {
-        Map<String, List<HttpMatcher>> tempMap = new HashMap<>();
+        final var builder = ImmutablePathMatcher.<List<HttpMatcher>> builder().handlerAccumulator(List::addAll);
         for (Map.Entry<String, PolicyMappingConfig> entry : permissions.entrySet()) {
             HttpSecurityPolicy checker = permissionCheckers.get(entry.getValue().policy);
             if (checker == null) {
@@ -108,34 +109,19 @@ private void init(Map<String, PolicyMappingConfig> permissions,
                     if (!path.startsWith("/")) {
                         path = rootPath + path;
                     }
-                    if (tempMap.containsKey(path)) {
-                        HttpMatcher m = new HttpMatcher(entry.getValue().authMechanism.orElse(null),
-                                new HashSet<>(entry.getValue().methods.orElse(Collections.emptyList())),
-                                checker);
-                        tempMap.get(path).add(m);
-                    } else {
-                        HttpMatcher m = new HttpMatcher(entry.getValue().authMechanism.orElse(null),
-                                new HashSet<>(entry.getValue().methods.orElse(Collections.emptyList())),
-                                checker);
-                        List<HttpMatcher> perms = new ArrayList<>();
-                        tempMap.put(path, perms);
-                        perms.add(m);
-                        if (path.endsWith("/*")) {
-                            String stripped = path.substring(0, path.length() - 2);
-                            pathMatcher.addPrefixPath(stripped.isEmpty() ? "/" : stripped, perms);
-                        } else if (path.endsWith("*")) {
-                            pathMatcher.addPrefixPath(path.substring(0, path.length() - 1), perms);
-                        } else {
-                            pathMatcher.addExactPath(path, perms);
-                        }
-                    }
+                    HttpMatcher m = new HttpMatcher(entry.getValue().authMechanism.orElse(null),
+                            new HashSet<>(entry.getValue().methods.orElse(Collections.emptyList())), checker);
+                    List<HttpMatcher> perms = new ArrayList<>();
+                    perms.add(m);
+                    builder.addPath(path, perms);
                 }
             }
         }
+        return builder.build();
     }
 
     public List<HttpSecurityPolicy> findPermissionCheckers(RoutingContext context) {
-        PathMatcher.PathMatch<List<HttpMatcher>> toCheck = pathMatcher.match(context.normalizedPath());
+        PathMatch<List<HttpMatcher>> toCheck = pathMatcher.match(context.normalizedPath());
         if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) {
             return Collections.emptyList();
         }
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java
new file mode 100644
index 0000000000000..1778c24d81a94
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java
@@ -0,0 +1,346 @@
+package io.quarkus.vertx.http.runtime.security;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.BiConsumer;
+
+import io.quarkus.runtime.configuration.ConfigurationException;
+import io.quarkus.vertx.http.runtime.security.ImmutableSubstringMap.SubstringMatch;
+
+/**
+ * Handler that dispatches to a given handler based on a match of the path.
+ */
+public class ImmutablePathMatcher<T> {
+
+    private final ImmutableSubstringMap<T> paths;
+    private final Map<String, T> exactPathMatches;
+
+    /**
+     * lengths of all registered paths
+     */
+    private final int[] lengths;
+    private final T defaultHandler;
+    private final boolean hasPathWithInnerWildcard;
+    private final boolean hasExactPathMatches;
+
+    private ImmutablePathMatcher(T defaultHandler, ImmutableSubstringMap<T> paths, Map<String, T> exactPathMatches,
+            int[] lengths, boolean hasPathWithInnerWildcard) {
+        this.defaultHandler = defaultHandler;
+        this.paths = paths;
+        this.lengths = Arrays.copyOf(lengths, lengths.length);
+        this.hasPathWithInnerWildcard = hasPathWithInnerWildcard;
+        if (exactPathMatches.isEmpty()) {
+            this.exactPathMatches = null;
+            this.hasExactPathMatches = false;
+        } else {
+            this.exactPathMatches = Map.copyOf(exactPathMatches);
+            this.hasExactPathMatches = true;
+        }
+    }
+
+    /**
+     * Matches a path against the registered handlers.
+     *
+     * @param path The relative path to match
+     * @return The match. This will never be null, however if none matched its value field will be
+     */
+    public PathMatch<T> match(String path) {
+        if (hasExactPathMatches) {
+            T match = exactPathMatches.get(path);
+            if (match != null) {
+                return new PathMatch<>(path, "", match);
+            }
+        }
+
+        int length = path.length();
+        for (int pathLength : lengths) {
+            if (pathLength == length) {
+                SubstringMatch<T> next = paths.get(path, length);
+                if (next != null) {
+                    return new PathMatch<>(path, "", next.getValue());
+                }
+            } else if (pathLength < length) {
+                char c = path.charAt(pathLength);
+                // pathLength == 1 means prefix path is / because prefix path always starts with /
+                // which means it's default handler match, but if there is at least
+                // one path with inner wildcard, we need to check for paths like /*/one
+                if (c == '/' || (hasPathWithInnerWildcard && pathLength == 1)) {
+
+                    //String part = path.substring(0, pathLength);
+                    SubstringMatch<T> next = paths.get(path, pathLength);
+                    if (next != null) {
+                        return new PathMatch<>(next.getKey(), path.substring(pathLength), next.getValue());
+                    }
+                }
+            }
+        }
+        return new PathMatch<>("", path, defaultHandler);
+    }
+
+    public static <T> ImmutablePathMatcherBuilder<T> builder() {
+        return new ImmutablePathMatcherBuilder<>();
+    }
+
+    public static final class PathMatch<T> {
+        private final String matched;
+        private final String remaining;
+        private final T value;
+
+        public PathMatch(String matched, String remaining, T value) {
+            this.matched = matched;
+            this.remaining = remaining;
+            this.value = value;
+        }
+
+        /**
+         * @deprecated because it can't be supported with inner wildcard without cost. It's unlikely this method is
+         *             used by anyone as users don't get in touch with this class. If there is legit use case, please
+         *             open Quarkus issue.
+         */
+        @Deprecated
+        public String getRemaining() {
+            return remaining;
+        }
+
+        public String getMatched() {
+            return matched;
+        }
+
+        public T getValue() {
+            return value;
+        }
+    }
+
+    public static class ImmutablePathMatcherBuilder<T> {
+
+        private static final String STRING_PATH_SEPARATOR = "/";
+        private final Map<String, T> exactPathMatches = new HashMap<>();
+        private final Map<String, Path<T>> pathsWithWildcard = new HashMap<>();
+        private BiConsumer<T, T> handlerAccumulator;
+
+        private ImmutablePathMatcherBuilder() {
+        }
+
+        /**
+         * @param handlerAccumulator policies defined with same path are accumulated, this way, you can define
+         *        more than one policy of one path (e.g. one for POST method, one for GET method)
+         * @return ImmutablePathMatcherBuilder
+         */
+        public ImmutablePathMatcherBuilder<T> handlerAccumulator(BiConsumer<T, T> handlerAccumulator) {
+            this.handlerAccumulator = handlerAccumulator;
+            return this;
+        }
+
+        public ImmutablePathMatcher<T> build() {
+            T defaultHandler = null;
+            SubstringMap<T> paths = new SubstringMap<>();
+            boolean hasPathWithInnerWildcard = false;
+            // process paths with a wildcard first, that way we only create inner path matcher when really needed
+            for (Path<T> p : pathsWithWildcard.values()) {
+                T handler = null;
+                ImmutablePathMatcher<SubstringMatch<T>> subPathMatcher = null;
+
+                if (p.prefixPathHandler != null) {
+                    handler = p.prefixPathHandler;
+                    if (STRING_PATH_SEPARATOR.equals(p.path)) {
+                        defaultHandler = p.prefixPathHandler;
+                    }
+                }
+
+                if (p.pathsWithInnerWildcard != null) {
+                    if (!hasPathWithInnerWildcard) {
+                        hasPathWithInnerWildcard = true;
+                    }
+                    // create path matcher for sub-path after inner wildcard: /one/*/three/four => /three/four
+                    var builder = new ImmutablePathMatcherBuilder<SubstringMatch<T>>();
+                    if (handlerAccumulator != null) {
+                        builder.handlerAccumulator(
+                                new BiConsumer<SubstringMatch<T>, SubstringMatch<T>>() {
+                                    @Override
+                                    public void accept(SubstringMatch<T> match1, SubstringMatch<T> match2) {
+                                        if (match2.hasSubPathMatcher()) {
+                                            // this should be impossible to happen since these matches are created
+                                            // right in this 'build()' method, but let's make sure of that
+                                            throw new IllegalStateException(
+                                                    String.format("Failed to merge sub-matches with key '%s' for path '%s'",
+                                                            match1.getKey(), p.originalPath));
+                                        }
+                                        handlerAccumulator.accept(match1.getValue(), match2.getValue());
+                                    }
+                                });
+                    }
+                    for (PathWithInnerWildcard<T> p1 : p.pathsWithInnerWildcard) {
+                        builder.addPath(p.originalPath, p1.remaining, new SubstringMatch<>(p1.remaining, p1.handler));
+                    }
+                    subPathMatcher = builder.build();
+                }
+
+                paths.put(p.path, handler, subPathMatcher);
+            }
+            int[] lengths = buildLengths(paths.keys());
+            return new ImmutablePathMatcher<>(defaultHandler, paths.asImmutableMap(), exactPathMatches, lengths,
+                    hasPathWithInnerWildcard);
+        }
+
+        /**
+         * Two sorts of paths are accepted:
+         * - exact path matches (without wildcard); these are matched first and Quarkus does no magic,
+         * request path must exactly match
+         * - paths with one or more wildcard:
+         * - ending wildcard matches zero or more path segment
+         * - inner wildcard matches exactly one path segment
+         * few notes:
+         * - it's key to understand only segments are matched, for example '/one*' will not match request path '/ones'
+         * - path patterns '/one*' and '/one/*' are one and the same thing as we only match path segments and '/one*'
+         * in fact means 'either /one or /one/any-number-of-path-segments'
+         * - paths are matched on longer-prefix-wins basis
+         * - what we call 'prefix' is in fact path to the first wildcard
+         * - if there is a path after first wildcard like in the '/one/*\/three' pattern ('/three' is remainder)
+         * path pattern is considered longer than the '/one/*' pattern and wins for request path '/one/two/three'
+         * - more specific pattern wins and wildcard is always less specific than any other path segment character,
+         * therefore path '/one/two/three*' will win over '/one/*\/three*' for request path '/one/two/three/four'
+         *
+         * @param path normalized path
+         * @param handler prefix path handler
+         * @return self
+         */
+        public ImmutablePathMatcherBuilder<T> addPath(String path, T handler) {
+            return addPath(path, path, handler);
+        }
+
+        private ImmutablePathMatcherBuilder<T> addPath(String originalPath, String path, T handler) {
+            if (!path.startsWith("/")) {
+                String errMsg = "Path must always start with a path separator, but was '" + path + "'";
+                if (!originalPath.equals(path)) {
+                    errMsg += " created from original path pattern '" + originalPath + "'";
+                }
+                throw new IllegalArgumentException(errMsg);
+            }
+            final int wildcardIdx = path.indexOf('*');
+            if (wildcardIdx == -1) {
+                addExactPath(path, handler);
+            } else {
+                addWildcardPath(path, handler, wildcardIdx, originalPath);
+            }
+            return this;
+        }
+
+        private void addWildcardPath(String path, T handler, int wildcardIdx, String originalPath) {
+            final int lastIdx = path.length() - 1;
+            final String pathWithWildcard;
+            final String pathAfter1stWildcard;
+
+            if (lastIdx == wildcardIdx) {
+                // ends with a wildcard => it's a prefix path
+                pathWithWildcard = path;
+                pathAfter1stWildcard = null;
+            } else {
+                // contains at least one inner wildcard: /one/*/three, /one/two/*/four/*, ...
+                // the inner wildcard represents exactly one path segment
+                pathWithWildcard = path.substring(0, wildcardIdx + 1);
+                pathAfter1stWildcard = path.substring(wildcardIdx + 1);
+
+                // validate that inner wildcard is enclosed with path separators like: /one/*/two
+                // anything like: /one*/two, /one/*two/, /one/tw*o/ is not allowed
+                if (!pathWithWildcard.endsWith("/*") || !pathAfter1stWildcard.startsWith("/")) {
+                    throw new ConfigurationException("HTTP permission path '" + originalPath + "' contains inner "
+                            + "wildcard enclosed with a path character other than a separator. The inner wildcard "
+                            + "must represent exactly one path segment. Please see this Quarkus guide for more "
+                            + "information: https://quarkus.io/guides/security-authorize-web-endpoints-reference");
+                }
+            }
+
+            final String pathWithoutWildcard;
+            if (pathWithWildcard.endsWith("/*")) {
+                // remove /*
+                String stripped = pathWithWildcard.substring(0, pathWithWildcard.length() - 2);
+                pathWithoutWildcard = stripped.isEmpty() ? "/" : stripped;
+            } else {
+                // remove *
+                pathWithoutWildcard = pathWithWildcard.substring(0, pathWithWildcard.length() - 1);
+            }
+
+            Path<T> p = pathsWithWildcard.computeIfAbsent(pathWithoutWildcard, Path::new);
+            p.originalPath = originalPath;
+            if (pathAfter1stWildcard == null) {
+                p.addPrefixPath(handler, handlerAccumulator);
+            } else {
+                p.addPathWithInnerWildcard(pathAfter1stWildcard, handler);
+            }
+        }
+
+        private void addExactPath(final String path, final T handler) {
+            if (path.isEmpty()) {
+                throw new IllegalArgumentException("Path not specified");
+            }
+            if (exactPathMatches.containsKey(path) && handlerAccumulator != null) {
+                handlerAccumulator.accept(exactPathMatches.get(path), handler);
+            } else {
+                exactPathMatches.put(path, handler);
+            }
+        }
+
+        private static int[] buildLengths(Iterable<String> keys) {
+            final Set<Integer> lengths = new TreeSet<>(new Comparator<Integer>() {
+                @Override
+                public int compare(Integer o1, Integer o2) {
+                    return -o1.compareTo(o2);
+                }
+            });
+            for (String p : keys) {
+                lengths.add(p.length());
+            }
+
+            int[] lengthArray = new int[lengths.size()];
+            int pos = 0;
+            for (int i : lengths) {
+                lengthArray[pos++] = i;
+            }
+            return lengthArray;
+        }
+    }
+
+    private static class Path<T> {
+        private final String path;
+        private String originalPath = null;
+        private T prefixPathHandler = null;
+        private List<PathWithInnerWildcard<T>> pathsWithInnerWildcard = null;
+
+        private Path(String path) {
+            this.path = path;
+        }
+
+        private void addPathWithInnerWildcard(String remaining, T handler) {
+            if (pathsWithInnerWildcard == null) {
+                pathsWithInnerWildcard = new ArrayList<>();
+            }
+            pathsWithInnerWildcard.add(new PathWithInnerWildcard<>(remaining, handler));
+        }
+
+        public void addPrefixPath(T prefixPathHandler, BiConsumer<T, T> handlerAccumulator) {
+            Objects.requireNonNull(prefixPathHandler);
+            if (this.prefixPathHandler != null && handlerAccumulator != null) {
+                handlerAccumulator.accept(this.prefixPathHandler, prefixPathHandler);
+            } else {
+                this.prefixPathHandler = prefixPathHandler;
+            }
+        }
+    }
+
+    private static class PathWithInnerWildcard<T> {
+        private final String remaining;
+        private final T handler;
+
+        private PathWithInnerWildcard(String remaining, T handler) {
+            this.remaining = remaining;
+            this.handler = handler;
+        }
+    }
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutableSubstringMap.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutableSubstringMap.java
new file mode 100644
index 0000000000000..fd0e572b83cfd
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutableSubstringMap.java
@@ -0,0 +1,135 @@
+package io.quarkus.vertx.http.runtime.security;
+
+import java.util.Arrays;
+
+import io.quarkus.vertx.http.runtime.security.ImmutablePathMatcher.PathMatch;
+
+/**
+ * A string keyed map that can be accessed as a substring, eliminating the need to allocate a new string
+ * to do a key comparison against.
+ */
+public class ImmutableSubstringMap<V> {
+
+    private static final int ALL_BUT_LAST_BIT = ~1;
+    private final Object[] table;
+
+    ImmutableSubstringMap(Object[] table) {
+        this.table = Arrays.copyOf(table, table.length);
+    }
+
+    @SuppressWarnings("unchecked")
+    public SubstringMatch<V> get(String key, int length) {
+        if (key.length() < length) {
+            throw new IllegalArgumentException();
+        }
+        int hash = hash(key, length);
+        int pos = tablePos(table, hash);
+        int start = pos;
+        while (table[pos] != null) {
+            if (doEquals((String) table[pos], key, length)) {
+                SubstringMatch<V> match = (SubstringMatch<V>) table[pos + 1];
+                if (match == null) {
+                    return null;
+                }
+                if (match.hasSubPathMatcher) {
+                    // consider request path '/one/two/three/four/five'
+                    // 'match.key' (which is prefix path) never ends with a slash, e.g. 'match.key=/one/two'
+                    // which means index 'match.key.length()' is index of the last char of the '/one/two/' sub-path
+                    // considering we are looking for a path segment after '/one/two/*', that is the first char
+                    // of the '/four/five' sub-path, the separator index must be greater than 'match.key.length() + 1'
+                    if (key.length() > (match.key.length() + 1)) {
+                        // let say match key is '/one/two'
+                        // then next path segment is '/four' and '/three' is skipped
+                        // for path pattern was like: '/one/two/*/four/five'
+                        int nextPathSegmentIdx = key.indexOf('/', match.key.length() + 1);
+                        if (nextPathSegmentIdx != -1) {
+                            // following the example above, 'nextPath' would be '/four/five'
+                            // and * matched 'three' path segment characters
+                            String nextPath = key.substring(nextPathSegmentIdx);
+                            PathMatch<SubstringMatch<V>> subMatch = match.subPathMatcher.match(nextPath);
+                            if (subMatch.getValue() != null) {
+                                return subMatch.getValue();
+                            }
+                        }
+                    }
+
+                    if (match.value == null) {
+                        // paths with inner wildcard didn't match
+                        // and there is no prefix path with ending wildcard either
+                        return null;
+                    }
+                }
+                // prefix path with ending wildcard: /one/two*
+                return match;
+            }
+            pos += 2;
+            if (pos >= table.length) {
+                pos = 0;
+            }
+            if (pos == start) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    static int tablePos(Object[] table, int hash) {
+        return (hash & (table.length - 1)) & ALL_BUT_LAST_BIT;
+    }
+
+    static boolean doEquals(String s1, String s2, int length) {
+        if (s1.length() != length || s2.length() < length) {
+            return false;
+        }
+        for (int i = 0; i < length; ++i) {
+            if (s1.charAt(i) != s2.charAt(i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static int hash(String value, int length) {
+        if (length == 0) {
+            return 0;
+        }
+        int h = 0;
+        for (int i = 0; i < length; i++) {
+            h = 31 * h + value.charAt(i);
+        }
+        return h;
+    }
+
+    public static final class SubstringMatch<V> {
+        private final String key;
+        private final V value;
+        private final boolean hasSubPathMatcher;
+        private final ImmutablePathMatcher<SubstringMatch<V>> subPathMatcher;
+
+        SubstringMatch(String key, V value) {
+            this.key = key;
+            this.value = value;
+            this.subPathMatcher = null;
+            this.hasSubPathMatcher = false;
+        }
+
+        SubstringMatch(String key, V value, ImmutablePathMatcher<SubstringMatch<V>> subPathMatcher) {
+            this.key = key;
+            this.value = value;
+            this.subPathMatcher = subPathMatcher;
+            this.hasSubPathMatcher = subPathMatcher != null;
+        }
+
+        public String getKey() {
+            return key;
+        }
+
+        public V getValue() {
+            return value;
+        }
+
+        boolean hasSubPathMatcher() {
+            return hasSubPathMatcher;
+        }
+    }
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatcher.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatcher.java
index 032a9f91fe118..c069fe2645a0c 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatcher.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatcher.java
@@ -7,6 +7,8 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 
+import io.quarkus.vertx.http.runtime.security.ImmutableSubstringMap.SubstringMatch;
+
 /**
  * Handler that dispatches to a given handler based of a prefix match of the path.
  * <p>
@@ -16,7 +18,10 @@
  * <p>
  *
  * @author Stuart Douglas
+ *
+ * @deprecated use {@link ImmutablePathMatcher} instead
  */
+@Deprecated
 public class PathMatcher<T> {
 
     private static final String STRING_PATH_SEPARATOR = "/";
@@ -55,7 +60,7 @@ public PathMatch<T> match(String path) {
         final int[] lengths = this.lengths;
         for (int pathLength : lengths) {
             if (pathLength == length) {
-                SubstringMap.SubstringMatch<T> next = paths.get(path, length);
+                SubstringMatch<T> next = paths.get(path, length);
                 if (next != null) {
                     return new PathMatch<>(path, "", next.getValue());
                 }
@@ -64,7 +69,7 @@ public PathMatch<T> match(String path) {
                 if (c == '/') {
 
                     //String part = path.substring(0, pathLength);
-                    SubstringMap.SubstringMatch<T> next = paths.get(path, pathLength);
+                    SubstringMatch<T> next = paths.get(path, pathLength);
                     if (next != null) {
                         return new PathMatch<>(next.getKey(), path.substring(pathLength), next.getValue());
                     }
@@ -117,7 +122,7 @@ public T getExactPath(final String path) {
     public T getPrefixPath(final String path) {
 
         // enable the prefix path mechanism to return the default handler
-        SubstringMap.SubstringMatch<T> match = paths.get(path);
+        SubstringMatch<T> match = paths.get(path);
         if (PathMatcher.STRING_PATH_SEPARATOR.equals(path) && match == null) {
             return this.defaultHandler;
         }
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/SubstringMap.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/SubstringMap.java
index 8ee402e48e220..75867de490fe0 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/SubstringMap.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/SubstringMap.java
@@ -1,10 +1,16 @@
 package io.quarkus.vertx.http.runtime.security;
 
+import static io.quarkus.vertx.http.runtime.security.ImmutableSubstringMap.doEquals;
+import static io.quarkus.vertx.http.runtime.security.ImmutableSubstringMap.hash;
+import static io.quarkus.vertx.http.runtime.security.ImmutableSubstringMap.tablePos;
+
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.NoSuchElementException;
 
+import io.quarkus.vertx.http.runtime.security.ImmutableSubstringMap.SubstringMatch;
+
 /**
  * A string keyed map that can be accessed as a substring, eliminating the need to allocate a new string
  * to do a key comparison against.
@@ -17,19 +23,27 @@
  * @author Stuart Douglas
  */
 public class SubstringMap<V> {
-    private static final int ALL_BUT_LAST_BIT = ~1;
 
     private volatile Object[] table = new Object[16];
     private int size;
 
+    /**
+     * @deprecated use {@link ImmutablePathMatcher}
+     */
+    @Deprecated
     public SubstringMatch<V> get(String key, int length) {
         return get(key, length, false);
     }
 
+    /**
+     * @deprecated use {@link ImmutablePathMatcher}
+     */
+    @Deprecated
     public SubstringMatch<V> get(String key) {
         return get(key, key.length(), false);
     }
 
+    @SuppressWarnings("unchecked")
     private SubstringMatch<V> get(String key, int length, boolean exact) {
         if (key.length() < length) {
             throw new IllegalArgumentException();
@@ -59,26 +73,19 @@ private SubstringMatch<V> get(String key, int length, boolean exact) {
         return null;
     }
 
-    private int tablePos(Object[] table, int hash) {
-        return (hash & (table.length - 1)) & ALL_BUT_LAST_BIT;
-    }
-
-    private boolean doEquals(String s1, String s2, int length) {
-        if (s1.length() != length || s2.length() < length) {
-            return false;
-        }
-        for (int i = 0; i < length; ++i) {
-            if (s1.charAt(i) != s2.charAt(i)) {
-                return false;
-            }
-        }
-        return true;
+    /**
+     * @deprecated use {@link ImmutablePathMatcher}
+     */
+    @Deprecated
+    public synchronized void put(String key, V value) {
+        put(key, value, null);
     }
 
-    public synchronized void put(String key, V value) {
+    void put(String key, V value, ImmutablePathMatcher<SubstringMatch<V>> subPathMatcher) {
         if (key == null) {
             throw new NullPointerException();
         }
+
         Object[] newTable;
         if (table.length / (double) size < 4 && table.length != Integer.MAX_VALUE) {
             newTable = new Object[table.length << 1];
@@ -91,11 +98,15 @@ public synchronized void put(String key, V value) {
             newTable = new Object[table.length];
             System.arraycopy(table, 0, newTable, 0, table.length);
         }
-        doPut(newTable, key, new SubstringMap.SubstringMatch<>(key, value));
+        doPut(newTable, key, new SubstringMatch<>(key, value, subPathMatcher));
         this.table = newTable;
         size++;
     }
 
+    /**
+     * @deprecated use {@link ImmutablePathMatcher}
+     */
+    @Deprecated
     public synchronized V remove(String key) {
         if (key == null) {
             throw new NullPointerException();
@@ -133,33 +144,30 @@ private void doPut(Object[] newTable, String key, Object value) {
         newTable[pos + 1] = value;
     }
 
+    /**
+     * @deprecated use {@link ImmutablePathMatcher}
+     */
+    @Deprecated
     public Map<String, V> toMap() {
         Map<String, V> map = new HashMap<>();
         Object[] t = this.table;
         for (int i = 0; i < t.length; i += 2) {
             if (t[i] != null) {
-                map.put((String) t[i], ((SubstringMatch<V>) t[i + 1]).value);
+                map.put((String) t[i], ((SubstringMatch<V>) t[i + 1]).getValue());
             }
         }
         return map;
     }
 
+    /**
+     * @deprecated use {@link ImmutablePathMatcher}
+     */
+    @Deprecated
     public synchronized void clear() {
         size = 0;
         table = new Object[16];
     }
 
-    private static int hash(String value, int length) {
-        if (length == 0) {
-            return 0;
-        }
-        int h = 0;
-        for (int i = 0; i < length; i++) {
-            h = 31 * h + value.charAt(i);
-        }
-        return h;
-    }
-
     public Iterable<String> keys() {
         return new Iterable<String>() {
             @Override
@@ -206,21 +214,8 @@ public void remove() {
 
     }
 
-    public static final class SubstringMatch<V> {
-        private final String key;
-        private final V value;
-
-        public SubstringMatch(String key, V value) {
-            this.key = key;
-            this.value = value;
-        }
-
-        public String getKey() {
-            return key;
-        }
-
-        public V getValue() {
-            return value;
-        }
+    ImmutableSubstringMap<V> asImmutableMap() {
+        return new ImmutableSubstringMap<>(table);
     }
+
 }
diff --git a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java
new file mode 100644
index 0000000000000..9cc89a3e3bd32
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java
@@ -0,0 +1,511 @@
+package io.quarkus.vertx.http.runtime;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.runtime.configuration.ConfigurationException;
+import io.quarkus.vertx.http.runtime.security.ImmutablePathMatcher;
+
+public class PathMatcherTest {
+
+    private static final Object HANDLER = new Object();
+
+    @Test
+    public void testPrefixPathWithEndingWildcard() {
+        ImmutablePathMatcher<Object> matcher = ImmutablePathMatcher.builder().addPath("/one/two/*", HANDLER).build();
+        assertMatched(matcher, "/one/two");
+        assertMatched(matcher, "/one/two/");
+        assertMatched(matcher, "/one/two/three");
+        assertNotMatched(matcher, "/one/twothree");
+        assertNotMatched(matcher, "/one/tw");
+        assertNotMatched(matcher, "/one");
+        assertNotMatched(matcher, "/");
+        assertNotMatched(matcher, "");
+        final Object exactPathMatcher1 = new Object();
+        final Object exactPathMatcher2 = new Object();
+        final Object exactPathMatcher3 = new Object();
+        final Object prefixPathMatcher1 = new Object();
+        final Object prefixPathMatcher2 = new Object();
+        matcher = ImmutablePathMatcher.builder().addPath("/one/two/*", prefixPathMatcher1)
+                .addPath("/one/two/three", exactPathMatcher1).addPath("/one/two", exactPathMatcher2)
+                .addPath("/one/two/three*", prefixPathMatcher2).addPath("/one/two/three/four", exactPathMatcher3).build();
+        assertMatched(matcher, "/one/two/three", exactPathMatcher1);
+        assertMatched(matcher, "/one/two", exactPathMatcher2);
+        assertMatched(matcher, "/one/two/three/four", exactPathMatcher3);
+        assertMatched(matcher, "/one/two/three/fou", prefixPathMatcher2);
+        assertMatched(matcher, "/one/two/three/four/", prefixPathMatcher2);
+        assertMatched(matcher, "/one/two/three/five", prefixPathMatcher2);
+        assertMatched(matcher, "/one/two/three/", prefixPathMatcher2);
+        assertMatched(matcher, "/one/two/thre", prefixPathMatcher1);
+        assertMatched(matcher, "/one/two/", prefixPathMatcher1);
+        assertNotMatched(matcher, "/one/tw");
+        assertNotMatched(matcher, "/one/");
+        assertNotMatched(matcher, "/");
+        assertNotMatched(matcher, "");
+    }
+
+    @Test
+    public void testPrefixPathDefaultHandler() {
+        final Object defaultHandler = new Object();
+        ImmutablePathMatcher<Object> matcher = ImmutablePathMatcher.builder().addPath("/one/two*", HANDLER)
+                .addPath("/*", defaultHandler).addPath("/q*", HANDLER).build();
+        assertMatched(matcher, "/", defaultHandler);
+        assertMatched(matcher, "", defaultHandler);
+        assertMatched(matcher, "0", defaultHandler);
+        assertMatched(matcher, "/q");
+        assertMatched(matcher, "/q/dev-ui");
+        assertMatched(matcher, "/qE", defaultHandler);
+        assertMatched(matcher, "/one/two");
+        assertMatched(matcher, "/one/two/three");
+        assertMatched(matcher, "/one/twothree", defaultHandler);
+        final Object exactPathMatcher1 = new Object();
+        final Object exactPathMatcher2 = new Object();
+        final Object exactPathMatcher3 = new Object();
+        final Object prefixPathMatcher1 = new Object();
+        final Object prefixPathMatcher2 = new Object();
+        matcher = ImmutablePathMatcher.builder().addPath("/one/two/*", prefixPathMatcher1).addPath("/*", defaultHandler)
+                .addPath("/one/two/three", exactPathMatcher1).addPath("/one/two", exactPathMatcher2)
+                .addPath("/one/two/three*", prefixPathMatcher2).addPath("/one/two/three/four", exactPathMatcher3).build();
+        assertMatched(matcher, "/one/two/three", exactPathMatcher1);
+        assertMatched(matcher, "/one/two", exactPathMatcher2);
+        assertMatched(matcher, "/one/two/three/four", exactPathMatcher3);
+        assertMatched(matcher, "/one/two/three/fou", prefixPathMatcher2);
+        assertMatched(matcher, "/one/two/three/four/", prefixPathMatcher2);
+        assertMatched(matcher, "/one/two/three/five", prefixPathMatcher2);
+        assertMatched(matcher, "/one/two/three/", prefixPathMatcher2);
+        assertMatched(matcher, "/one/two/thre", prefixPathMatcher1);
+        assertMatched(matcher, "/one/two/", prefixPathMatcher1);
+        assertMatched(matcher, "/one/tw", defaultHandler);
+        assertMatched(matcher, "/one/", defaultHandler);
+        assertMatched(matcher, "/", defaultHandler);
+        assertMatched(matcher, "", defaultHandler);
+    }
+
+    @Test
+    public void testPrefixPathsNoDefaultHandlerNoExactPath() {
+        final Object handler1 = new Object();
+        final Object handler2 = new Object();
+        final ImmutablePathMatcher<Object> matcher = ImmutablePathMatcher.builder().addPath("/one/two*", handler1)
+                .addPath("/q*", handler2).build();
+        assertNotMatched(matcher, "/");
+        assertNotMatched(matcher, "");
+        assertNotMatched(matcher, "0");
+        assertMatched(matcher, "/q", handler2);
+        assertMatched(matcher, "/q/dev-ui", handler2);
+        assertNotMatched(matcher, "/qE");
+        assertMatched(matcher, "/one/two", handler1);
+        assertMatched(matcher, "/one/two/three", handler1);
+        assertMatched(matcher, "/one/two/", handler1);
+        assertNotMatched(matcher, "/one/twothree");
+    }
+
+    @Test
+    public void testSpecialChars() {
+        // strictly speaking query params are not part of request path passed to the matcher
+        // but here they are treated like any other character different from path separator
+        final Object handler1 = new Object();
+        final Object handler2 = new Object();
+        final Object handler3 = new Object();
+        final Object handler4 = new Object();
+        final Object handler5 = new Object();
+        // with default handler
+        ImmutablePathMatcher<Object> matcher = ImmutablePathMatcher.builder().addPath("/one/two#three", handler2)
+                .addPath("/one/two?three=four", handler1).addPath("/one/*/three?one\\\\\\=two", handler3)
+                .addPath("/one/two#three*", handler4).addPath("/*/two#three*", handler5).addPath("/*", HANDLER)
+                .build();
+        assertMatched(matcher, "/one/two#three", handler2);
+        assertMatched(matcher, "/one/two?three=four", handler1);
+        assertMatched(matcher, "/one/any-value/three?one\\\\\\=two", handler3);
+        assertMatched(matcher, "/one/two/three?one\\\\\\=two", handler3);
+        assertMatched(matcher, "/one/two/three?one\\=two");
+        assertMatched(matcher, "/one/two/three?one\\\\\\=two-three");
+        assertMatched(matcher, "/one/two/three?one");
+        assertMatched(matcher, "/one/two/three?");
+        assertMatched(matcher, "/one/two#three?");
+        assertMatched(matcher, "/one/two#thre");
+        assertMatched(matcher, "/one/two");
+        assertMatched(matcher, "/one/two?three=four#");
+        assertMatched(matcher, "/one/two?three=fou");
+        assertMatched(matcher, "/one/two#three/", handler4);
+        assertMatched(matcher, "/one/two#three/christmas!", handler4);
+        assertMatched(matcher, "/one/two#thre");
+        assertMatched(matcher, "/one1/two#three", handler5);
+        assertMatched(matcher, "/one1/two#three/", handler5);
+        assertMatched(matcher, "/one1/two#three/christmas!", handler5);
+        assertMatched(matcher, "/one1/two#thre");
+        // no default handler
+        matcher = ImmutablePathMatcher.builder().addPath("/one/two#three", handler2)
+                .addPath("/one/two?three=four", handler1).addPath("/one/*/three?one\\\\\\=two", handler3)
+                .addPath("/one/two#three*", handler4).addPath("/*/two#three*", handler5).build();
+        assertMatched(matcher, "/one/two#three", handler2);
+        assertMatched(matcher, "/one/two?three=four", handler1);
+        assertMatched(matcher, "/one/any-value/three?one\\\\\\=two", handler3);
+        assertMatched(matcher, "/one/two/three?one\\\\\\=two", handler3);
+        assertNotMatched(matcher, "/one/two/three?one\\=two");
+        assertNotMatched(matcher, "/one/two/three?one\\\\\\=two-three");
+        assertNotMatched(matcher, "/one/two/three?one");
+        assertNotMatched(matcher, "/one/two/three?");
+        assertNotMatched(matcher, "/one/two#three?");
+        assertNotMatched(matcher, "/one/two#thre");
+        assertNotMatched(matcher, "/one/two");
+        assertNotMatched(matcher, "/one/two?three=four#");
+        assertNotMatched(matcher, "/one/two?three=fou");
+        assertMatched(matcher, "/one/two#three/", handler4);
+        assertMatched(matcher, "/one/two#three/christmas!", handler4);
+        assertNotMatched(matcher, "/one/two#thre");
+        assertMatched(matcher, "/one1/two#three", handler5);
+        assertMatched(matcher, "/one1/two#three/", handler5);
+        assertMatched(matcher, "/one1/two#three/christmas!", handler5);
+        assertNotMatched(matcher, "/one1/two#thre");
+    }
+
+    @Test
+    public void testInnerWildcardsWithExactMatches() {
+        final Object handler1 = new Object();
+        final Object handler2 = new Object();
+        final Object handler3 = new Object();
+        final Object handler4 = new Object();
+        final Object handler5 = new Object();
+        final Object handler6 = new Object();
+        final Object handler7 = new Object();
+        final Object handler8 = new Object();
+        final ImmutablePathMatcher<Object> matcher = ImmutablePathMatcher.builder().addPath("/one/two", handler1)
+                .addPath("/one/two/three", handler2).addPath("/one/two/three/four", handler3)
+                .addPath("/", handler4).addPath("/*", HANDLER).addPath("/one/two/*/four", handler5)
+                .addPath("/one/*/three/four", handler6).addPath("/*/two/three/four", handler7)
+                .addPath("/*/two", handler8).build();
+        assertMatched(matcher, "/one/two", handler1);
+        assertMatched(matcher, "/one/two/three", handler2);
+        assertMatched(matcher, "/one/two/three/four", handler3);
+        assertMatched(matcher, "/", handler4);
+        assertMatched(matcher, "");
+        assertMatched(matcher, "no-one-likes-us");
+        assertMatched(matcher, "/one/two/we-do-not-care/four", handler5);
+        assertMatched(matcher, "/one/two/we-do-not-care/four/4");
+        assertMatched(matcher, "/one/we-are-millwall/three/four", handler6);
+        assertMatched(matcher, "/1-one/we-are-millwall/three/four");
+        assertMatched(matcher, "/super-millwall/two/three/four", handler7);
+        assertMatched(matcher, "/super-millwall/two/three/four/");
+        assertMatched(matcher, "/super-millwall/two/three/four/1");
+        assertMatched(matcher, "/from-the-den/two", handler8);
+        assertMatched(matcher, "/from-the-den/two2");
+    }
+
+    @Test
+    public void testInnerWildcardsOnly() {
+        final Object handler1 = new Object();
+        final Object handler2 = new Object();
+        final Object handler3 = new Object();
+        final Object handler4 = new Object();
+        final Object handler5 = new Object();
+        // with default path handler
+        ImmutablePathMatcher<Object> matcher = ImmutablePathMatcher.builder().addPath("/*/two", handler2)
+                .addPath("/*/*/three", handler1).addPath("/one/*/three", handler3)
+                .addPath("/one/two/*/four", handler4).addPath("/one/two/three/*/five", handler5)
+                .addPath("/*", HANDLER).build();
+        assertMatched(matcher, "/any-value");
+        assertMatched(matcher, "/one/two/three/four/five", handler5);
+        assertMatched(matcher, "/one/two/three/4/five", handler5);
+        assertMatched(matcher, "/one/two/three/sergey/five", handler5);
+        assertMatched(matcher, "/one/two/three/sergey/five-ish");
+        assertMatched(matcher, "/one/two/three/sergey/five/");
+        assertMatched(matcher, "/one/two/three/four", handler4);
+        assertMatched(matcher, "/one/two/3/four", handler4);
+        assertMatched(matcher, "/one/two/three", handler3);
+        assertMatched(matcher, "/one/2/three", handler3);
+        assertMatched(matcher, "/one/some-very-long-text/three", handler3);
+        assertMatched(matcher, "/two");
+        assertMatched(matcher, "/two/two", handler2);
+        assertMatched(matcher, "/2/two", handler2);
+        assertMatched(matcher, "/ho-hey/two", handler2);
+        assertMatched(matcher, "/ho-hey/two2");
+        assertMatched(matcher, "/ho-hey/two2/");
+        assertMatched(matcher, "/ho-hey/two/");
+        assertMatched(matcher, "/ho-hey/hey-ho/three", handler1);
+        assertMatched(matcher, "/1/2/three", handler1);
+        assertMatched(matcher, "/1/two/three", handler1);
+        assertMatched(matcher, "/1/two/three/");
+        assertMatched(matcher, "/1/two/three/f");
+        // no default path handler
+        matcher = ImmutablePathMatcher.builder().addPath("/*/two", handler2)
+                .addPath("/*/*/three", handler1).addPath("/one/*/three", handler3)
+                .addPath("/one/two/*/four", handler4).addPath("/one/two/three/*/five", handler5).build();
+        assertNotMatched(matcher, "/any-value");
+        assertMatched(matcher, "/one/two/three/four/five", handler5);
+        assertMatched(matcher, "/one/two/three/4/five", handler5);
+        assertMatched(matcher, "/one/two/three/sergey/five", handler5);
+        assertNotMatched(matcher, "/one/two/three/sergey/five-ish");
+        assertNotMatched(matcher, "/one/two/three/sergey/five/");
+        assertMatched(matcher, "/one/two/three/four", handler4);
+        assertMatched(matcher, "/one/two/3/four", handler4);
+        assertMatched(matcher, "/one/two/three", handler3);
+        assertMatched(matcher, "/one/2/three", handler3);
+        assertMatched(matcher, "/one/some-very-long-text/three", handler3);
+        assertNotMatched(matcher, "/two");
+        assertMatched(matcher, "/two/two", handler2);
+        assertMatched(matcher, "/2/two", handler2);
+        assertMatched(matcher, "/ho-hey/two", handler2);
+        assertNotMatched(matcher, "/ho-hey/two2");
+        assertNotMatched(matcher, "/ho-hey/two2/");
+        assertNotMatched(matcher, "/ho-hey/two/");
+        assertMatched(matcher, "/ho-hey/hey-ho/three", handler1);
+        assertMatched(matcher, "/1/2/three", handler1);
+        assertMatched(matcher, "/1/two/three", handler1);
+        assertNotMatched(matcher, "/1/two/three/");
+        assertNotMatched(matcher, "/1/two/three/f");
+    }
+
+    @Test
+    public void testInnerWildcardWithEndingWildcard() {
+        final Object handler1 = new Object();
+        final Object handler2 = new Object();
+        final Object handler3 = new Object();
+        final Object handler4 = new Object();
+        final Object handler5 = new Object();
+        // with default handler
+        ImmutablePathMatcher<Object> matcher = ImmutablePathMatcher.builder().addPath("/*/two/*", handler1)
+                .addPath("/one/*/*", handler2).addPath("/one/two/*/four*", handler3)
+                .addPath("/one/*/three/*", handler4).addPath("/one/two/*/*", handler5)
+                .addPath("/*", HANDLER).build();
+        assertMatched(matcher, "/one/two/three/four/five/six", handler3);
+        assertMatched(matcher, "/one/two/three/four/five", handler3);
+        assertMatched(matcher, "/one/two/three/four/", handler3);
+        assertMatched(matcher, "/one/two/three/four", handler3);
+        assertMatched(matcher, "/one/two/3/four", handler3);
+        assertMatched(matcher, "/one/two/three/4", handler5);
+        assertMatched(matcher, "/one/two/three/4/", handler5);
+        assertMatched(matcher, "/one/two/three/4/five", handler5);
+        assertMatched(matcher, "/one/2/three/four/five", handler4);
+        assertMatched(matcher, "/one/2/3/four/five", handler2);
+        assertMatched(matcher, "/1/two/three/four/five", handler1);
+        assertMatched(matcher, "/1/2/three/four/five");
+    }
+
+    @Test
+    public void testInnerWildcardsDefaultHandler() {
+        final Object handler1 = new Object();
+        final Object handler2 = new Object();
+        final Object handler3 = new Object();
+        // both default root path handler and sub-path handler
+        ImmutablePathMatcher<Object> matcher = ImmutablePathMatcher.builder().addPath("/*/*", handler1)
+                .addPath("/*/*/three", handler3).addPath("/*", handler2).build();
+        assertMatched(matcher, "/one/two/three", handler3);
+        assertMatched(matcher, "/one/two/four", handler1);
+        assertMatched(matcher, "/one/two", handler1);
+        assertMatched(matcher, "/one", handler2);
+        assertMatched(matcher, "/", handler2);
+    }
+
+    @Test
+    public void testInvalidPathPattern() {
+        // path must start with a path separator
+        assertThrows(IllegalArgumentException.class, () -> ImmutablePathMatcher.builder().addPath("one", HANDLER).build());
+        // inner wildcard must always be only path segment character
+        assertThrows(ConfigurationException.class, () -> ImmutablePathMatcher.builder().addPath("/one*/", HANDLER).build());
+        assertThrows(ConfigurationException.class, () -> ImmutablePathMatcher.builder().addPath("/*one/", HANDLER).build());
+        assertThrows(ConfigurationException.class, () -> ImmutablePathMatcher.builder().addPath("/o*ne/", HANDLER).build());
+        assertThrows(ConfigurationException.class, () -> ImmutablePathMatcher.builder().addPath("/one/*two/", HANDLER).build());
+        assertThrows(ConfigurationException.class, () -> ImmutablePathMatcher.builder().addPath("/one/*two/", HANDLER).build());
+        assertThrows(ConfigurationException.class, () -> ImmutablePathMatcher.builder().addPath("/one/two*/", HANDLER).build());
+        assertThrows(ConfigurationException.class,
+                () -> ImmutablePathMatcher.builder().addPath("/one/*two*/", HANDLER).build());
+    }
+
+    @Test
+    public void testExactPathHandlerMerging() {
+        List<String> handler1 = new ArrayList<>();
+        handler1.add("Neo");
+        List<String> handler2 = new ArrayList<>();
+        handler2.add("Trinity");
+        List<String> handler3 = new ArrayList<>();
+        handler3.add("Morpheus");
+        var matcher = ImmutablePathMatcher.<List<String>> builder().handlerAccumulator(List::addAll)
+                .addPath("/exact-path", handler1).addPath("/exact-path", handler2)
+                .addPath("/exact-not-matched", handler3).build();
+        var handler = matcher.match("/exact-path").getValue();
+        assertNotNull(handler);
+        assertTrue(handler.contains("Neo"));
+        assertTrue(handler.contains("Trinity"));
+        assertEquals(2, handler.size());
+        handler = matcher.match("/exact-not-matched").getValue();
+        assertNotNull(handler);
+        assertEquals(1, handler.size());
+    }
+
+    @Test
+    public void testPrefixPathHandlerMerging() {
+        List<String> handler1 = new ArrayList<>();
+        handler1.add("Neo");
+        List<String> handler2 = new ArrayList<>();
+        handler2.add("Trinity");
+        List<String> handler3 = new ArrayList<>();
+        handler3.add("Morpheus");
+        List<String> handler4 = new ArrayList<>();
+        handler4.add("AgentSmith");
+        List<String> handler5 = new ArrayList<>();
+        handler5.add("TheOracle");
+        List<String> handler6 = new ArrayList<>();
+        handler6.add("AgentBrown");
+        var matcher = ImmutablePathMatcher.<List<String>> builder().handlerAccumulator(List::addAll).addPath("/path*", handler1)
+                .addPath("/path*", handler2).addPath("/path/*", handler3).addPath("/path/", handler4)
+                .addPath("/path/*/", handler5).addPath("/*", handler6).build();
+        var handler = matcher.match("/path").getValue();
+        assertNotNull(handler);
+        assertTrue(handler.contains("Neo"));
+        assertTrue(handler.contains("Trinity"));
+        assertTrue(handler.contains("Morpheus"));
+        assertEquals(3, handler.size());
+        handler = matcher.match("/path/").getValue();
+        assertNotNull(handler);
+        assertEquals(1, handler.size());
+        assertTrue(handler.contains("AgentSmith"));
+        handler = matcher.match("/stuart").getValue();
+        assertNotNull(handler);
+        assertEquals(1, handler.size());
+        assertTrue(handler.contains("AgentBrown"));
+        handler = matcher.match("/path/ozzy/").getValue();
+        assertNotNull(handler);
+        assertEquals(1, handler.size());
+        assertTrue(handler.contains("TheOracle"));
+    }
+
+    @Test
+    public void testInnerWildcardPathHandlerMerging() {
+        List<String> handler1 = new ArrayList<>();
+        handler1.add("Neo");
+        List<String> handler2 = new ArrayList<>();
+        handler2.add("Trinity");
+        List<String> handler3 = new ArrayList<>();
+        handler3.add("Morpheus");
+        List<String> handler4 = new ArrayList<>();
+        handler4.add("AgentSmith");
+        List<String> handler5 = new ArrayList<>();
+        handler5.add("TheOracle");
+        List<String> handler6 = new ArrayList<>();
+        handler6.add("AgentBrown");
+        List<String> handler7 = new ArrayList<>();
+        handler7.add("TheOperator");
+        List<String> handler8 = new ArrayList<>();
+        handler8.add("TheSpoonBoy");
+        List<String> handler9 = new ArrayList<>();
+        handler9.add("TheArchitect");
+        List<String> handler10 = new ArrayList<>();
+        handler10.add("KeyMan");
+        List<String> handler11 = new ArrayList<>();
+        handler11.add("Revolutions");
+        List<String> handler12 = new ArrayList<>();
+        handler12.add("Reloaded-1");
+        List<String> handler13 = new ArrayList<>();
+        handler13.add("Reloaded-2");
+        List<String> handler14 = new ArrayList<>();
+        handler14.add("Reloaded-3");
+        var matcher = ImmutablePathMatcher.<List<String>> builder().handlerAccumulator(List::addAll)
+                .addPath("/*/one", handler1).addPath("/*/*", handler2).addPath("/*/*", handler3)
+                .addPath("/*/one", handler4).addPath("/*/two", handler5).addPath("/*", handler6)
+                .addPath("/one/*/three", handler7).addPath("/one/*", handler8).addPath("/one/*/*", handler9)
+                .addPath("/one/*/three", handler10).addPath("/one/*/*", handler11)
+                .addPath("/one/*/*/*", handler12).addPath("/one/*/*/*", handler13)
+                .addPath("/one/*/*/*", handler14).build();
+        var handler = matcher.match("/one/two/three").getValue();
+        assertNotNull(handler);
+        assertEquals(2, handler.size());
+        assertTrue(handler.contains("TheOperator"));
+        assertTrue(handler.contains("KeyMan"));
+        handler = matcher.match("/one/two/three/four").getValue();
+        assertNotNull(handler);
+        assertEquals(3, handler.size());
+        assertTrue(handler.contains("Reloaded-1"));
+        assertTrue(handler.contains("Reloaded-2"));
+        assertTrue(handler.contains("Reloaded-3"));
+        handler = matcher.match("/one/2/3").getValue();
+        assertNotNull(handler);
+        assertEquals(2, handler.size());
+        assertTrue(handler.contains("TheArchitect"));
+        assertTrue(handler.contains("Revolutions"));
+        handler = matcher.match("/one/two").getValue();
+        assertNotNull(handler);
+        assertEquals(1, handler.size());
+        assertTrue(handler.contains("TheSpoonBoy"));
+        handler = matcher.match("/1/one").getValue();
+        assertNotNull(handler);
+        assertEquals(2, handler.size());
+        assertTrue(handler.contains("Neo"));
+        assertTrue(handler.contains("AgentSmith"));
+        handler = matcher.match("/1/two").getValue();
+        assertNotNull(handler);
+        assertEquals(1, handler.size());
+        assertTrue(handler.contains("TheOracle"));
+        handler = matcher.match("/father-brown").getValue();
+        assertNotNull(handler);
+        assertEquals(1, handler.size());
+        assertTrue(handler.contains("AgentBrown"));
+        handler = matcher.match("/welcome/to/the/jungle").getValue();
+        assertNotNull(handler);
+        assertEquals(2, handler.size());
+        assertTrue(handler.contains("Trinity"));
+        assertTrue(handler.contains("Morpheus"));
+    }
+
+    @Test
+    public void testDefaultHandlerInnerWildcardAndEndingWildcard() {
+        // calling it default handler inner wildcard because first '/' path is matched and then '/one*'
+        // '/one*' is matched as prefix path
+        final ImmutablePathMatcher<Object> matcher = ImmutablePathMatcher.builder().addPath("/*/one*", HANDLER).build();
+        assertMatched(matcher, "/1/one");
+        assertMatched(matcher, "/2/one");
+        assertMatched(matcher, "/3/one");
+        assertMatched(matcher, "/4/one");
+        assertMatched(matcher, "/4/one");
+        assertMatched(matcher, "/1/one/");
+        assertMatched(matcher, "/1/one/two");
+        assertNotMatched(matcher, "/");
+        assertNotMatched(matcher, "/1");
+        assertNotMatched(matcher, "/1/");
+        assertNotMatched(matcher, "/1/one1");
+        assertNotMatched(matcher, "/1/two");
+        assertNotMatched(matcher, "/1/on");
+    }
+
+    @Test
+    public void testDefaultHandlerOneInnerWildcard() {
+        // calling it default handler inner wildcard because first '/' path is matched and then '/one'
+        // '/one' is matched as exact path
+        final ImmutablePathMatcher<Object> matcher = ImmutablePathMatcher.builder().addPath("/*/one", HANDLER).build();
+        assertMatched(matcher, "/1/one");
+        assertMatched(matcher, "/2/one");
+        assertMatched(matcher, "/3/one");
+        assertMatched(matcher, "/4/one");
+        assertMatched(matcher, "/4/one");
+        assertNotMatched(matcher, "/");
+        assertNotMatched(matcher, "/1");
+        assertNotMatched(matcher, "/1/");
+        assertNotMatched(matcher, "/1/two");
+        assertNotMatched(matcher, "/1/one/");
+        assertNotMatched(matcher, "/1/one1");
+        assertNotMatched(matcher, "/1/on");
+        assertNotMatched(matcher, "/1/one/two");
+    }
+
+    private static void assertMatched(ImmutablePathMatcher<Object> matcher, String path, Object handler) {
+        var match = matcher.match(path);
+        assertEquals(handler, match.getValue());
+    }
+
+    private static void assertMatched(ImmutablePathMatcher<Object> matcher, String path) {
+        assertMatched(matcher, path, HANDLER);
+    }
+
+    private static <T> void assertNotMatched(ImmutablePathMatcher<T> matcher, String path) {
+        var match = matcher.match(path);
+        assertNull(match.getValue());
+    }
+
+}