DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Software Design and Architecture
  3. Microservices
  4. GraphQL in Microservices With Spring and Angular

GraphQL in Microservices With Spring and Angular

This article will help software developers decide whether REST or GraphQL is best for their project(s) based on their frontend and backend implementations.

Sven Loesekann user avatar by
Sven Loesekann
·
Dec. 23, 22 · Tutorial
Like (2)
Save
Tweet
Share
3.54K Views

Join the DZone community and get the full member experience.

Join For Free

The AngularAndSpringWithMaps project has been converted from REST endpoints to a GraphQL interface. The project uses Spring GraphQL to provide the backend interface. The Angular frontend uses the Angular  HttpClient to post the requests to the backend.

GraphQL vs REST From an Architectural Perspective

REST Endpoints

REST calls retrieve objects with their children. For different root objects, separate REST calls are sent. These calls can accumulate with the number of different root objects that the frontend requests. With relationships between the objects, sequential calls become necessary.

REST Endpoints

Rings for the Polygon ID’s are fetched. Then the locations for the ring ID’s are fetched, which is a real REST design. It would also be possible to create two endpoints. 

  1. An endpoint that returns the ‘CompanySite.’
  2. An endpoint that returns the ‘CompanySite’ with all its children.

Then the client can request the data from the right endpoint and gets what it needs with one request.

With a growing number of requirements, the number of endpoints for clients grows. The effort to support the new endpoints will grow accordingly.

GraphQL Interface

GraphQL can support queries where the requested result object can be specified.

GraphQL Interface

The client that requests a ‘CompanySite’ can specify in the query the Polygons/Rings/Locations that the server needs to provide as children to the client. That creates a very flexible interface for the frontend but needs a backend that can support all possible queries in an efficient implementation.

Considerations for a Microservice Architecture

REST endpoints and GraphQL interfaces can have different use cases.

REST Endpoints

The different REST endpoints make it easier to implement additional logic for the results. For example, calculating sums and statistics for paying users of the requested objects. The more specialized endpoints can support these features easier.

Specialized Servers

GraphQL Interfaces

The GraphQL interface is more generic in its result structure. That makes it a good fit for an implementation that collects the requested data and returns it to the client. Smarter clients that implement more of the business logic are a good fit for this architecture. They can request the data required to provide the required features.

QraphQL Server

The GraphQL server can use databases or other servers to read its data. The data reads need to be optimized for all query shapes (support for partial data requests). Due to the generic interface, the GraphQL server will have less knowledge of its clients. 

GraphQL Implementation in a Microservice

The AngularAndSpringWithMaps project uses a GraphQL interface.

GraphQL in an Angular Frontend

GraphQL clients for Angular are available, but in this use case, Angular Services with the HttpClient are used. The GraphQLService implements the queries and mutations: 

TypeScript
 
export interface GraphqlOptions {
    operationName: string;
    query: string;
    variables?: { [key: string]: any};
}

@Injectable()
export class GraphqlService {

  constructor(private http: HttpClient) { }
  
  public query<T>(options: GraphqlOptions): Observable<T> {
    return this.http
      .post<{ data: T }>(`/graphql`, {
	    operationName: options.operationName,
        query: options.query,
        variables: options.variables,
      })
      .pipe(map((d) => d.data));
  }

  public mutate<T>(options: GraphqlOptions): Observable<any> {
    return this.http
      .post<{ data: T }>(`/graphql`, {
	    operationName: options.operationName,
        query: options.query,
        variables: options.variables,
      })
      .pipe(map((d) => d.data));
  }
}


The GraphQLOptions interface defines the parameters for the ‘query’ and ‘mutate’ methods of the GraphQLService. 

The GraphQLService is provided by the MapsModule and implements the ‘query’ and ‘mutate’ methods. Both methods post the content of the GraphQLOptions to the /graphql path. The posted object contains the properties ‘operationName,’ ‘query,’ and ‘variables.’ The returned result uses an RxJS pipe to unwrap the data in the result.

The CompanySiteService uses the GraphQLService to request the ‘CompanySites’ by ‘name’ and ‘year:’

TypeScript
 
@Injectable()
export class CompanySiteService {
   ...
   public findByTitleAndYear(title: string, year: number): 
      Observable<CompanySite[]> {
         const options = { operationName: 'getCompanySiteByTitle', 
            query: 'query getCompanySiteByTitle($title: String!, $year: 
            Long!) { getCompanySiteByTitle(title: $title, year: $year) { 
            id, title, atDate }}', variables: { 'title': title, 'year': 
            year } } as GraphqlOptions;
	 return this.mapResult<CompanySite[],CompanySite[]>
            (this.graphqlService.query<CompanySite[]>(options), 
            options.operationName);
   }

   public findByTitleAndYearWithChildren(title: string, year: number):  
      Observable<CompanySite[]> {
      const options = { operationName: 'getCompanySiteByTitle', query: 
         'query getCompanySiteByTitle($title: String!, $year: Long!) { 
         getCompanySiteByTitle(title: $title, year: $year) { id, title, 
         atDate, polygons { id, fillColor, borderColor, title, longitude, 
         latitude,rings{ id, primaryRing,locations { id, longitude, 
         latitude}}}}}', variables: { 'title': title, 'year': year } } as 
         GraphqlOptions;
      return this.mapResult<CompanySite[],CompanySite[]>
         (this.graphqlService.query<CompanySite[]>(options), 
         options.operationName);
  }
  ...


The findByTitleAndYear(...) method creates the GraphQLOptions with a query that requests ‘id, title, atDate’ of the ‘CompanySite.’

The findByTitleAndYearWithChildren(..) method creates the GraphQLOptions with a query that requests the ‘CompanySites’ with all its children and their properties and children. 

The ‘operationName’ is used in the mapResult(...) method to unwrap the result. 

Conclusion Frontend

In this use case, Angular provided the needed tools, and GraphQL is easy enough to use with Angular only. To develop the GraphQL queries for the backend, GraphQL (/graphiql) can help. 

GraphQL in the Spring Boot Backend

Spring Boot provides good support for GraphQL backend implementations. Due to the many options in the queries, the implementation has to be much more flexible than a REST implementation.

GraphQL Schema

The schema is in the schema.graphqls file:

Plain Text
 
scalar Date
scalar BigDecimal
scalar Long

input CompanySiteIn {
	id: ID!
	title: String!
	atDate: Date!
	polygons: [PolygonIn]
}

type CompanySiteOut {
	id: ID!
	title: String!
	atDate: Date!
	polygons: [PolygonOut]
}

...

type Query {
    getMainConfiguration: MainConfigurationOut
    getCompanySiteByTitle(title: String!, year: Long!): [CompanySiteOut]
    getCompanySiteById(id: ID!): CompanySiteOut
}

type Mutation {
   upsertCompanySite(companySite: CompanySiteIn!): CompanySiteOut
   resetDb: Boolean
   deletePolygon(companySiteId: ID!, polygonId: ID!): Boolean
}


The ‘scalar Date’ imports the ‘Date’ datatype from the graphql-java-extended-scalars library for use in the schema file. The same is implemented for the data types ‘BigDecimal’ and ‘Lo.’

The ‘input’ datatypes are for the upsertCompanySite(...) to call in the ‘Mutation’ type.

The ‘type’ datatypes are for the return types of the ‘Mutation’ or ‘Query’ calls. 

The ‘Query’ type contains the functions to read the data from the GraphQL interface. 

The ‘Mutation’ type contains the functions to change the data with the GraphQL interface. The ‘CompanySiteIn’ input type is a tree of data that is posted to the interface. The interface returns the updated data in ‘CompanySiteOut.’

GraphQL Configuration

The additional types of the graphql-java-extended-scalars library need to be configured in Spring GraphQL. That is done in the GraphQLConfig:

Java
 
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
   RuntimeWiringConfigurer result = wiringBuilder -> 
      wiringBuilder.scalar(ExtendedScalars.Date)		    
         .scalar(ExtendedScalars.DateTime)
         .scalar(ExtendedScalars.GraphQLBigDecimal)		  
         .scalar(ExtendedScalars.GraphQLBigInteger)
         .scalar(ExtendedScalars.GraphQLByte)
         .scalar(ExtendedScalars.GraphQLChar)
         .scalar(ExtendedScalars.GraphQLLong)		 
         .scalar(ExtendedScalars.GraphQLShort)
         .scalar(ExtendedScalars.Json)
         .scalar(ExtendedScalars.Locale)
	 .scalar(ExtendedScalars.LocalTime)
         .scalar(ExtendedScalars.NegativeFloat)
	 .scalar(ExtendedScalars.NegativeInt)
         .scalar(ExtendedScalars.NonNegativeFloat)
	 .scalar(ExtendedScalars.NonNegativeInt)
         .scalar(ExtendedScalars.NonPositiveFloat)
 	 .scalar(ExtendedScalars.NonPositiveInt)
         .scalar(ExtendedScalars.Object)
         .scalar(ExtendedScalars.Time)
	 .scalar(ExtendedScalars.Url)
         .scalar(ExtendedScalars.UUID);
      return result;
}


The additional datatypes of the graphql-java-extended-scalars library are registered with the ‘wiringBuilder.’

GraphQL Controllers

The ‘Query’ and ‘Mutation’ requests are implemented in the ConfigurationController and the CompanySiteController. The CompanySiteController is shown here:

Java
 
@Controller
public class CompanySiteController {
   private static final Logger LOGGER =  
      LoggerFactory.getLogger(CompanySite.class);
   private final CompanySiteService companySiteService;
   private final EntityDtoMapper entityDtoMapper;
   private record Selections(boolean withPolygons, boolean withRings,
      boolean withLocations) {}

   public CompanySiteController(CompanySiteService companySiteService, 
      EntityDtoMapper entityDtoMapper) {
      this.companySiteService = companySiteService;
      this.entityDtoMapper = entityDtoMapper;
   }

   @QueryMapping
   public Mono<List<CompanySiteDto>> getCompanySiteByTitle(
      @Argument String title, @Argument Long year,
      DataFetchingEnvironment dataFetchingEnvironment) {
      Selections selections = createSelections(dataFetchingEnvironment);
      List<CompanySiteDto> companySiteDtos = 
         this.companySiteService.findCompanySiteByTitleAndYear(title, 
             year, selections.withPolygons(), selections.withRings(), 
             selections.withLocations()).stream().map(companySite -> 
                this.entityDtoMapper.mapToDto(companySite))
		.collect(Collectors.toList());
      return Mono.just(companySiteDtos);
   }

   private Selections createSelections(DataFetchingEnvironment 
      dataFetchingEnvironment) {
      boolean addPolygons = 
         dataFetchingEnvironment.getSelectionSet().contains("polygons");
      boolean addRings = 
         dataFetchingEnvironment.getSelectionSet().getFields().stream()
	    .anyMatch(sf -> "rings".equalsIgnoreCase(sf.getName()));
      boolean addLocations = 
         dataFetchingEnvironment.getSelectionSet().getFields().stream()
 	 .filter(sf -> "rings".equalsIgnoreCase(sf.getName()))
         .flatMap(sf -> Stream.of(sf.getSelectionSet()))
	 .anyMatch(sf -> sf.contains("locations"));
      Selections selections = new Selections(addPolygons, addRings, 
         addLocations);
      return selections;
}


The CompanySiteController implements the ‘CompanySite’ related methods of the GraphQL schema. It gets the CompanySiteService and the EntityDtoMapper injected.

The getCompanySiteByTitle(…) method gets the arguments with the annotations that are defined in the GraphQL schema and the DataFetchingEnvironment. The DataFetchingEnvironment is used to call the createSelections(…) method. The createSelections(…) method uses the SelectionSets to find the requested children, like ‘polygons,’ ‘rings,’ and ‘locations’ and creates a record with booleans to return the requested child types. The method findCompanySiteByTitleAndYear(…) of the CompanySiteService is then used to read the ‘CompanySites.’ The result entities are then mapped with the EntityDtoMapper into DTO’s and are returned. The EntityDtoMapper is able to handle not initialized JPA properties. Spring GraphQL filters and returns only the requested properties of the DTO’s.

GraphQL Services

The CompanySiteService implements the flexible selection structure of the data:

Java
 
@Transactional
@Service
public class CompanySiteService {
   private final CompanySiteRepository companySiteRepository;
   private final PolygonRepository polygonRepository;
   private final RingRepository ringRepository;
   private final LocationRepository locationRepository;
   private final DataFetcher<Iterable<CompanySite>> dataFetcherCs;
   private final EntityManager entityManager;

   public CompanySiteService(
      CompanySiteRepository companySiteRepository, 
      PolygonRepository polygonRepository, RingRepository ringRepository, 
      LocationRepository locationRepository, 
      EntityManager entityManager) {
      
      this.companySiteRepository = companySiteRepository;
      this.polygonRepository = polygonRepository;
      this.ringRepository = ringRepository;
      this.locationRepository = locationRepository;
      this.entityManager = entityManager;
   }

   public Collection<CompanySite> findCompanySiteByTitleAndYear(String 
      title, Long year, boolean withPolygons,
      boolean withRings, boolean withLocations) {
      
      if (title == null || title.length() < 2 || year == null) {
         return List.of();
      }
      LocalDate beginOfYear = LocalDate.of(year.intValue(), 1, 1);
      LocalDate endOfYear = LocalDate.of(year.intValue(), 12, 31);
      title = title.trim().toLowerCase();
      List<CompanySite> companySites = this.companySiteRepository
         .findByTitleFromTo(title, beginOfYear, endOfYear).stream()
         .peek(myCompanySite ->   
             this.entityManager.detach(myCompanySite)).toList();
      companySites = addEntities(withPolygons, withRings, withLocations,
         companySites);
      return companySites;
   }

   private List<CompanySite> addEntities(boolean withPolygons, boolean 
      withRings, boolean withLocations,	List<CompanySite> companySites) {
      
       if (withPolygons) {
          Map<Long, List<Polygon>> fetchPolygons = 
             this.fetchPolygons(companySites);
	  Map<Long, List<Ring>> fetchRings = !withRings ? Map.of() : 
             this.fetchRings(fetchPolygons.values()
                .stream().flatMap(List::stream).toList());
	  Map<Long, List<Location>> fetchLocations = !withLocations ? 
             Map.of() : this.fetchLocations(fetchRings.values()
                .stream().flatMap(List::stream).toList());
	  companySites.forEach(myCompanySite -> {
             myCompanySite.setPolygons(
               new HashSet<>(fetchPolygons.
                  getOrDefault(myCompanySite.getId(), List.of())));
	     if (withRings) { 
		myCompanySite.getPolygons()
                   .forEach(myPolygon -> {
		      myPolygon.setRings(
                         new HashSet<>(fetchRings.getOrDefault(
                            myPolygon.getId(), List.of())));
			if (withLocations) {
			   myPolygon.getRings().forEach(myRing -> {
			      myRing.setLocations(
			         new HashSet<>(fetchLocations
                                    .getOrDefault(myRing.getId(), 
                                        List.of())));
		          });
	               }
                });
	     }
         });
      }
      return companySites;
   }

   public Map<Long, List<Polygon>> fetchPolygons(List<CompanySite> 
      companySites) {
      
      List<Polygon> polygons = this.polygonRepository
	.findAllByCompanySiteIds(companySites.stream().map(cs -> 
           cs.getId()).collect(Collectors.toList()))
	   .stream().peek(myPolygon -> 
              this.entityManager.detach(myPolygon)).toList();
      return companySites.stream().map(CompanySite::getId)
         .map(myCsId -> Map.entry(findEntity(companySites, 
             myCsId).getId(), findPolygons(polygons, myCsId)))
	 .collect(Collectors.toMap(Map.Entry::getKey, 
            Map.Entry::getValue));
   }
...
}


The CompanySiteService gets the needed repositories and the EntityManager injected.

The method findCompanySiteByTitleAndYear(…) checks its parameters and then prepares the parameters of the findByTitleFromTo(…) method for the CompanySiteRepository. The method returns the matching ‘CompanySite’ entities and uses a Stream to detach them. The method addEntities(...) is then called to retrieve the child entities of the requested ‘CompanySite’ entities. For each child layer (such as ‘Polygons,’ ‘Rings,’ ‘Locations’) all child entities are collected. After all the parent entities are collected the children are selected in one query to avoid the ‘+1’ query problem. The child entities are also detached to support partial loading of the child entities and to support the DTO mapping. 

The fetchPolygons(…) method uses the findAllByCompanySiteIds(…) method of the PolygonRepository to select all the matching Polygons of all the ‘CompanySite ID’s.’ The Polygons are returned in a map with the ‘CompanySite ID’ as the key and the Polygon entity list as the value. 

GraphQL Repositories

To support the loading of the child entities by layer. The child entities have to be selected by parent ‘ID.’ That is, for example, done in the JPAPolygonRepository:

Java
 
public interface JpaPolygonRepository extends JpaRepository<Polygon, 
   Long>, QuerydslPredicateExecutor<Polygon> {

   @Query("select p from Polygon p inner join p.companySite cs 
              where cs.id in :ids")
   List<Polygon> findAllByCompanySiteIds(@Param("ids") List<Long> ids);
}


The findAllByCompanySiteIds(…) method creates a JQL query that joins the ‘CompanySite’ entity and selects all Polygons for the ‘CompanySite IDs.’ That means one query for all Polygons. Four layers (such as ‘CompanySite,’ ‘Polygon,’ ‘Ring,’ and ‘Location’) equal four queries.

Conclusion Backend

To avoid the ‘+1’ problem with entity loading and to load only the needed entities, this layered approach was needed. A REST endpoint that knows how many child layers have to be loaded can be implemented more efficiently (one query). The price to pay for that efficiency are more endpoints.

Conclusion

REST and GraphQL both have advantages and disadvantages.

  • Because the returned results are less flexible, REST can support more logic at the endpoints more easily.
  • GraphQL supports the specification of the requested results. That can be used to request more flexible results that can be smaller but needs a flexible backend to efficiently support all possible result shapes.

Each project needs to decide what architecture better fits the given requirements. For that decision, the requirements and the estimated development effort for the frontend and backend implementations need to be considered.

GraphQL AngularJS Interface (computing) microservice Spring Boot Software design Software architecture

Published at DZone with permission of Sven Loesekann. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Chaos Engineering Tutorial: Comprehensive Guide With Best Practices
  • File Uploads for the Web (2): Upload Files With JavaScript
  • Demystifying the Infrastructure as Code Landscape
  • Introduction Garbage Collection Java

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: