0

I need some guidance in designing an API wrapper for my backend APIs. I have tried to keep it as specific as possible.

Context: We have a project which supports certain file operations like edit, create, merge etc. All the services are exposed as rest APIs. Now I have to create an API wrapper over this (client library) in Java. I've been reading about DDD and trying to approach the problem using that.

As per my thinking, the core object in my project would be File, along with some minor DTOs for talking to the backend. Edit, create, merge will be the verbs here acting on my domain object. I want to make it as easy as possible for the external developer to integrate the API. I would like the design to be something like this:

For Creating a file : File.create()

For editing : File.edit()

Same for other operations

Also, I want to have the capability of chaining operations (along the lines of fluent interfaces) for readability

For. eg. if you want to create a file and then convert it, it should be something like : File.create().convert(Required params)

My problem is each of the operation is bulky and async. I don't wanna write all the async error handling logic in the File class. Chaining the methods like above wont be easy as well if they return CompletableFuture objects, and this will be harder to maintain.

Question: What is a better way of solving this problem?

I am not looking for a spoonfed design. I just want to be guided to a design approach which fits the scenario. Feel free to point if I am understanding DDD wrong.

0

DDD is used to model the core domain of your application, which in your case would be the actual implementation of the backend APIs. The client library and the REST APIs are layers on top of the domain model that facilitate communication with the outside world, they are not part of the domain layer themselves.

The File class in your client library is simply a proxy to the real entity in your backend. Its single responsibility is to provide a clean interface and hide the details of calling your REST API, so I don't see anything wrong with this class encapsulating async operations and error handling.

The backend is a different story. The File entity there is part of the domain model, which should be kept clear of any infrastructural code, and you would have a separate application layer that exposes the domain layer to the REST endpoint.

  • DDD can be used to model your application. It doesn't matter whether the application speaks to an HTTP backend or a database one. There is a Domain, even for the "client", and the code should reflect that, that is the core principle of DDD. – Robert Bräutigam Feb 1 at 10:46
  • @RobertBräutigam Of course every application has a "domain", but DDD is more than just that -- it is a specific set of principles and patterns that helps model complex domains. In fact, the very first chapter of the classic DDD book talks about how it is not the right approach to everything. – casablanca Feb 2 at 4:05
0

I think you're on the right path. If your domain is handling files, the DDD approach is basically to have the language of this domain drive the design.

When it comes to asynchronicity, there is however no way around CompletableFuture in Java. The whole point of that is, that you don't need to handle the error in your classes, the error will be automatically taken care of. You should choose an HTTP client that works with it natively, or make a wrapper, then your classes can essentially forget about it being asynchronous. Which is a pretty good deal.

It is a bit different to write, but not significantly. Instead of:

File.create(name)
   .convert(mapper);

You will have:

File.create(name)
   .thenCompose(f -> f.convert(mapper));

It's not that complicated for the functionality it provides in return.

Edit: A design idea that incorporates hiding CompletableFuture and chaining. This assumes the HTTP library is non-blocking (returns CompletableFuture).

public final class File {
    // The "Resource" here comes from a fictional HTTP library
    private final Function<Resource, CompletableFuture<Resource>> logic;

    private File(Function<Resource, CompletableFuture<Resource>> logic) {
        this.logic = logic;
    }

    public File() {
        this(CompletableFuture::completedFuture); // No-op
    }

    public File create(String name) {
        return new File(logic.andThen(c -> c.thenCompose(r -> {
            return r.post(name); // Or whatever
        })));
    }

    public File convert(Transformation tx) {
        return new File(logic.andThen(c -> c.thenCompose(r -> {
            return r.post(tx); // Or whatever
        })));
    }

    // Here the "terminal" operation
    public void write() {
        logic.apply(null).join();
    }
}

Of course there are some details missing but it shows how to assemble a complex operation without actually doing it, and then doing it all in the terminal operation.

  • Good to know I am on the right path. I agree with what you said about completable future. However, the external developer wont call my code like File.create().thenCompose If I want to chain the methods, I'd like to return File from each operation but I believe that won't be possible in this design, especially with all the async ops. Do you agree? – lazyloader Feb 1 at 10:54
  • Well, you could hide it a la "Streams". You would then have to define methods that just plug together the chain and then "terminal" operations that actually trigger the processing. For example File.create(name).convert(mapper) would not do anything yet, because that's pure chaining. If you want to actually do it, you'll need File.create(name).convert(mapper).write() or something similar, where the chaining stops. You would only have to deal with CompletableFuture directly in the terminal operation(s). – Robert Bräutigam Feb 1 at 11:10
  • That's exactly what I thought Robert :D. Doing something along the lines of the java stream api or rxjava. The problems I am facing with that approach is too many async calls being chained together, once the terminal operation is called. Moreover, the individual functions become kind of empty in that case with no real logic except maybe setting some flag, and if the methods are parameterised, it becomes more complicated. Are you aware of any resources suited for this kind of design? – lazyloader Feb 1 at 11:50
  • Here we can steal some ideas from functional programming, which does this all the time. Composing a lot of functions without actually doing anything until the actual call comes. I've made an edit to show how this might work in Java. – Robert Bräutigam Feb 1 at 13:44
  • Robert, this opens so many possibilities for my design. I've been using java streams for over a year now but couldn't think this way. Thanks a lot for this. It looks so intuitive now that I've seen it. I think I'll move the operations to some other class/interface since imo this conflicts with DDD for the File class since logic is not a behavior of the File, and the functions can be parameterized as well. But I get the gist of it.This also serves as a lesson for me, to see how what happens behind the scenes for the commonly used APIs. It's too good of a learning source to ignore. Thanks a lot! – lazyloader Feb 1 at 18:19

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.