API Composition (BFF) using Spring and Reactor

API Composition (BFF) using Spring and Reactor



Imagine I have two microservices and I want to implement the BFF (Backend for the Frontend) pattern within a Spring REST controller which uses WebFlux.



The domain objects from the 2 remote services are:


public class Comment
private Long id;
private String text;
private Long authorId;
private Long editorId;


public class Person
private Long id;
private String firstName;
private String lastName;



and the API Composer must return objects of following type:


public class ComposedComment
private String text;
private String authorFullName;
private String editorFullName;



For the sake of semplicity I wrote a Controller which simulates all the services in one.


@RestController
@RequestMapping("/api")
public class Controller

private static final List<Comment> ALL_COMMENTS = Arrays.asList(//
new Comment(1L, "Bla bla", 1L, null), //
new Comment(2L, "lorem ipsum", 2L, 3L), //
new Comment(3L, "a comment", 2L, 1L));
private static final Map<Long, Person> PERSONS;

static
PERSONS = new HashMap<>();
PERSONS.put(1L, new Person(1L, "John", "Smith"));
PERSONS.put(2L, new Person(2L, "Paul", "Black"));
PERSONS.put(3L, new Person(3L, "Maggie", "Green"));


private WebClient clientCommentService = WebClient.create("http://localhost:8080/api");
private WebClient clientPersonService = WebClient.create("http://localhost:8080/api");

@GetMapping("/composed/comments")
public Flux<ComposedComment> getComposedComments()
//This is the tricky part


private String extractFullName(Map<Long, Person> map, Long personId)
Person person = map.get(personId);
return person == null ? null : person.getFirstName() + " " + person.getLastName();


@GetMapping("/comments")
public ResponseEntity<List<Comment>> getAllComments()
return new ResponseEntity<List<Comment>>(ALL_COMMENTS, HttpStatus.OK);


@GetMapping("/persons/personIds")
public ResponseEntity<List<Person>> getPersonsByIdIn(@PathVariable("personIds") Set<Long> personIds)
List<Person> persons = personIds.stream().map(id -> PERSONS.get(id)).filter(person -> person != null)
.collect(Collectors.toList());
return new ResponseEntity<List<Person>>(persons, HttpStatus.OK);




My problem is I have just began with Reactor and I am not really sure of what I am doing.. This is the current version of my composer method:


@GetMapping("/composed/comments")
public Flux<ComposedComment> getComposedComments()
Flux<Comment> commentFlux = clientCommentService.get().uri("/comments").retrieve().bodyToFlux(Comment.class);
Set<Long> personIds = commentFlux.toStream().map(comment -> Arrays.asList(comment.getAuthorId(), comment.getEditorId())).flatMap(Collection::stream).filter(Objects::nonNull).collect(Collectors.toSet());
Map<Long, Person> personsById = clientPersonService.get().uri("/persons/ids", personIds.stream().map(Object::toString).collect(Collectors.joining(","))).retrieve().bodyToFlux(Person.class).collectMap(Person::getId).block();
return commentFlux.map(
comment -> new ComposedComment(
comment.getText(),
extractFullName(personsById, comment.getAuthorId()),
extractFullName(personsById, comment.getEditorId()))
);



It works, nevertheless I know I should make several transformations with map, flatMap and zip instead of invoking block() and toStream()... Can you please help me to rewrite this method correctly? :)





Avoid using toStream or other blocking operator. In your Spring 5 webflux environment you are never forced to block.
– Daniel Jipa
Aug 23 at 15:00


toStream





Hi @DanielJipa, thanks for your answer. The problem is, I cannot create an URI without first blocking on the mono/flux to get the String I will use as a uriVariables, as the WebClient.UriSpec doesn't have any reactive uri method: docs.spring.io/spring/docs/current/javadoc-api/org/… Or am I missing something? Thanks!
– Federico
Aug 25 at 8:16





Your domain model seems strange and also concatenate the personids to send it to the request path. As I understand there are multiple personids on the comment.
– Daniel Jipa
Aug 25 at 8:46





If thats the case on each comment on the flatMap concatenate the ids and send it to the personService.
– Daniel Jipa
Aug 25 at 8:50





yes it's right: each comment has 2 personIds: one for the author (which is never null) and one for the editor (which may be null if the comment didn't get edited). The idea is to collect all the comments from the commentService and then in 1 single further call collect all the persons which authored/modified any comments...
– Federico
Aug 25 at 8:50




2 Answers
2



You are returning null in your controller.
Replace it by returning the reactive stream instead.


return commentFlux.flatMap(comment -> ....)
....



Your controller signature return a


Flux<ComposedComment>



so make sure in the last return, you have to use flatMap or map to transform them to ComposedComment. You could think it as a promise chain, where you can do many flatMap, map in the implementation to transform to a final dataset.



Don't use subscribe in these situations, subscribe is suitable for demonstrating the invoking process of the reactive stream or in somewhere in the app where the result calling the method not needed directly as this controller



At this time you just return a reactive stream by using map, flatMap, collect, zip...... Just return the reactive stream (Mono, Flux<>) then spring-webflux will invoke them.





return null is just to let it compile. I understand the problem with subscribing, nevertheless I am confused by the map chain to get the composed objects with a single call to the personService.. :(
– Federico
Aug 22 at 9:06





I rewrote the method, now it's a bit better but not really reactive..
– Federico
Aug 22 at 16:17





Let me give you a rough example: map(classA -> classB) flatMap(classA -> Mono<ClassA>) map apply a function to transform the original item into another trivial item, flatMap can do more complex, it transform the original item into another stream (observable, Mono, or any reactive related stuff). You can assign your implementation into a Flux<ComposeComment> variable. Implement it gradually by inserting map, flatMap to see how they work, how the IDE notify errors then you will get it done!
– nghiaht
Aug 23 at 2:50



map(classA -> classB)


flatMap(classA -> Mono<ClassA>)



You should try the zip operator to compose the two publishers. And don't subscribe to the flux if you want to return it.


zip



If you can't use the zip because the second publisher depends on results from the first then use flatMap.


zip


flatMap



You can use the flatMap like this:


commentsFlux.flatMap(comment -> personService.getPersonsByIds(comment.getPersonId1() + "," + comment.getPersonId2())
//at this moment you have scope on both
.map(listOfTwoPersons -> new Composed(listOfTwoPersons, comment))



N.B. I didn't worked with webflux client and I am just guessing from your working example it knows to wrap to a Flux/Mono even if you return an entity or list of entities.





If I do this: Flux<Tuple2<Comment, List<Long>>> commentsWithPersonIds = Flux.zip(commentFlux, commentFlux.map(comment -> Arrays.asList( comment.getEditorId() == null ? comment.getAuthorId() : comment.getEditorId(), comment.getAuthorId()))//editorId can be null, authorId not ); I get Tuples with comments + ids of authors/editors. I should then make T2 flat, invoke the personService and somehow construct the ComposedComments?
– Federico
Aug 22 at 9:01






put into other words: after i get commentsWithPersonIds tuples <comment1;1,1>,<comment2;2,3>,<comment3;2,1> I think i should merge all the tuples into one <comment1, comment2, comment3;1,2,3>, then get the persons throug a map of T2s (here I invoke the person service through webclient) <comment1,comment2,comment3;person1,person2,person3> and finally map this tuple into a Flux<ComposedComments>.. Is it right? If yes, how can I make commentsWithPersonIds flat? Thanks in advance!
– Federico
Aug 22 at 14:18







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.

Popular posts from this blog

𛂒𛀶,𛀽𛀑𛂀𛃧𛂓𛀙𛃆𛃑𛃷𛂟𛁡𛀢𛀟𛁤𛂽𛁕𛁪𛂟𛂯,𛁞𛂧𛀴𛁄𛁠𛁼𛂿𛀤 𛂘,𛁺𛂾𛃭𛃭𛃵𛀺,𛂣𛃍𛂖𛃶 𛀸𛃀𛂖𛁶𛁏𛁚 𛂢𛂞 𛁰𛂆𛀔,𛁸𛀽𛁓𛃋𛂇𛃧𛀧𛃣𛂐𛃇,𛂂𛃻𛃲𛁬𛃞𛀧𛃃𛀅 𛂭𛁠𛁡𛃇𛀷𛃓𛁥,𛁙𛁘𛁞𛃸𛁸𛃣𛁜,𛂛,𛃿,𛁯𛂘𛂌𛃛𛁱𛃌𛂈𛂇 𛁊𛃲,𛀕𛃴𛀜 𛀶𛂆𛀶𛃟𛂉𛀣,𛂐𛁞𛁾 𛁷𛂑𛁳𛂯𛀬𛃅,𛃶𛁼

Edmonton

Crossroads (UK TV series)