How to refactor chain of asynchronous calls in vertx to avoid the callback hell

Your initial approach is not too bad actually.

To improve code for better "composability", you should change the handler input arg of each fooX method to something that extends Handler<AsyncResult<JsonObject>> (such as a Future) and returns the same handler as a result, so it becomes better usable in the `Future.compose because the passed-in handler could be used as return value for each compose:

 private <T extends Handler<AsyncResult<JsonObject>>> T foo1(String uuid, T aHandler) {
    JsonObject foo1 = new JsonObject().put("uuid", "foo1");
    aHandler.handle(Future.succeededFuture(foo1));
    return aHandler; //<-- return the handler here
}

Second, in order to access all three results in final stage, you have to declare the three futures outside the chain. Now you can chain the futures quiet nicely using the output of each foo method as result for each compose.

Future<JsonObject> futureFoo1 = Future.future();
Future<JsonObject> futureFoo2 = Future.future();
Future<JsonObject> futureFoo3 = Future.future();


foo1(uuid, futureFoo1).compose(resultFoo1 -> foo2(resultFoo1.getString("uuid"), futureFoo2))
                      .compose(resultFoo2 -> foo3(resultFoo2.getString("uuid"), futureFoo3))
                      .compose(resultFoo3 -> doSomething(futureFoo1.result(), //access results from 1st call
                                                         futureFoo2.result(), //access results from 2nd call 
                                                         resultFoo3,
                                                         Future.<JsonObject>future().setHandler(aHandler))); //pass the final result to the original handler

If you can't live with the "impurity" of this approach (defining the futures outside chain and modify them inside the function), you have to pass the original input values for each method (=the output of the previous call) along with result, but I doubt this would make the code more readable.

In order to change type in one compose method, you fooX method has to make the conversion, not returning the original handler, but a new Future with the different type

private Future<JsonArray> foo2(String uuid, Handler<AsyncResult<JsonObject>> aHandler) {
    JsonObject foo2 = new JsonObject();
    foo2.put("uuid", "foo2" + uuid);
    aHandler.handle(Future.succeededFuture(foo2));
    JsonArray arr = new JsonArray().add("123").add("456").add("789");
    return Future.succeededFuture(arr);
}