Microservices in the cloud, part two

Otavio Santana
Developer Relations
23 Aug 2019

Microservices are a hot topic in software design, and for good reason. They have plenty of advantages when it comes to handling infrastructure complexity, many of which were addressed in part one of our related Java post. Now, it’s time to talk about the code and design. In this post, we’ll take a deep dive into each module.

When creating an application, clean code means thinking about design and architecture. Architecture is the software process that handles flexibility, scalability, usability, security, and other points, so you have more time to focus on business rather than on technology. Some architecture examples include:

  • Serverless architecture. Application designs that incorporate third-party, Backend-as-a-Service (BaaS) services, and include custom code run in managed, ephemeral containers on a Functions-as-a-Service (FaaS) platform.
  • Event-driven architecture. A software architecture pattern promoting the production, detection, consumption of, and reaction to events.
  • Microservices architecture. A variant of the service-oriented architecture (SOA) style that structures an application as a collection of loosely coupled services. In a microservices architecture, services are fine grained and the protocols lightweight.

The design has a low-level duty that supervises the code, such as what each module is going to do, the class’s scope, the functions proposal, and so on.

  • SOLID. The five design principles that make software designs more understandable, flexible, and maintainable.
  • Design patterns. The ideal solutions to common problems in software design. Each pattern is like a blueprint you can customize to solve a particular design problem in your code.

Speaker service

The first service that we’ll cover is the Speaker service, which uses Thorntail and PostgreSQL. The Eclipse MicroProfile doesn’t have support for a relational database. However, we can use the specification from its older and more mature brother, Jakarta EE. As a first step, let’s define the Speaker entity.

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Objects;

@Entity
@Table(name = "speaker")
public class Speaker {

	@Id
	@GeneratedValue
	private Integer id;

	@Column
	private String name;

	@Column
	private String bio;

	@Column
	private String twitter;

	@Column
	private String github;

    //more code

}

Revisit our previous article that covers the JPA integration, using Transactional annotation and a CDI Interceptor to manage the transaction.

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;

@ApplicationScoped
public class SpeakerService {

	@Inject
	private EntityManager entityManager;

	@Transactional
	public Speaker insert(Speaker speaker) {
    	entityManager.persist(speaker);
    	return speaker;
	}

	@Transactional
	public void update(Speaker speaker) {
    	entityManager.persist(speaker);
	}

	@Transactional
	public void delete(Integer id) {
    	find(id).ifPresent(c -> entityManager.remove(c));
	}


	public Optional<Speaker> find(Integer id) {
    	return Optional.ofNullable(entityManager.find(Speaker.class, id));
	}

	public List<Speaker> findAll() {
    	String query = "select e from Speaker e";
    	return entityManager.createQuery(query).getResultList();
	}
}

We have a resource to expose the services through an HTTP request. We’ve created a layer on DTO to avoid losing the encapsulation.

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.status;

@Path("speakers")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public class SpeakerResource {

	@Inject
	private SpeakerService speakerService;

	@GET
	public List<SpeakerDTO> findAll() {
    	return speakerService.findAll()
            	.stream().map(SpeakerDTO::of)
            	.collect(Collectors.toList());
	}

	@GET
	@Path("{id}")
	public SpeakerDTO findById(@PathParam("id") Integer id) {
    	final Optional<Speaker> conference = speakerService.find(id);
    	return conference.map(SpeakerDTO::of).orElseThrow(this::notFound);
	}

	@PUT
	@Path("{id}")
	public SpeakerDTO update(@PathParam("id") Integer id, @Valid SpeakerDTO speakerUpdated) {
    	final Optional<Speaker> optional = speakerService.find(id);
    	final Speaker speaker = optional.orElseThrow(() -> notFound());
    	speaker.update(Speaker.of(speakerUpdated));
    	speakerService.update(speaker);
    	return SpeakerDTO.of(speaker);
	}

	@DELETE
	@Path("{id}")
	public Response remove(@PathParam("id") Integer id) {
    	speakerService.delete(id);
    	return status(NO_CONTENT).build();
	}

	@POST
	public SpeakerDTO insert(@Valid SpeakerDTO speaker) {
    	return SpeakerDTO.of(speakerService.insert(Speaker.of(speaker)));
	}

	private WebApplicationException notFound() {
    	return new WebApplicationException(Response.Status.NOT_FOUND);
	}
}

In the .platform.app.yaml file, we need to check that the speaker application has access to the PostgreSQL instance and anything else about the database that lets Platform.sh handle it.

relationships:
  postgresql: 'postgresql:postgresql'

Session service

The Session service will handle the contents of the conference, so that a user can find sessions related to topics like cloud and Java. This service uses Elasticsearch with Jakarta NoSQL and KumuluzEE.

import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;

import java.util.Objects;

@Entity
public class Session {

    @Id
    private String id;

    @Column
    private String name;

    @Column
    private String title;

    @Column
    private String description;

    @Column
    private String conference;

    @Column
    private Integer speaker;

}

We’re applying a distinct aspect of Elasticsearch to use it as a search engine. Jakarta NoSQL looks to the specialization that allows particular features for each NoSQL database. Therefore, we’ll have a mix of repository and ElasticsearchTemplate, a specialty of DocumentTemplate.

import jakarta.nosql.mapping.Repository;

import java.util.List;

public interface SessionRepository extends Repository<Session, String> {

	List<Session> findAll();
}


import org.elasticsearch.index.query.QueryBuilder;
import org.jnosql.artemis.elasticsearch.document.ElasticsearchTemplate;
import org.jnosql.artemis.util.StringUtils;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.status;
import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;

@Path("sessions")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON + "; charset=UTF-8")
public class SessionResource {

	private static Logger LOGGER = Logger.getLogger(SessionResource.class.getName());

	@Inject
	private SessionRepository speakerRepository;

	@Inject
	private ElasticsearchTemplate template;

	@GET
	public List<SessionDTO> findAll(@QueryParam("search") String search) {
    	LOGGER.info("searching with the field: " + search);
    	if (StringUtils.isNotBlank(search)) {
        	QueryBuilder queryBuilder = boolQuery()
                	.should(termQuery("name", search))
                	.should(termQuery("title", search))
                	.should(termQuery("description", search));

        	LOGGER.info("the query: " + queryBuilder);
        	List<Session> sessions = template.search(queryBuilder, "Session");
        	LOGGER.info("the result: " + sessions);
        	return sessions.stream()
                	.map(SessionDTO::of)
                	.collect(Collectors.toList());
    	}
    	return speakerRepository.findAll().stream()
            	.map(SessionDTO::of).collect(Collectors.toList());
	}

	@GET
	@Path("{id}")
	public Session findById(@PathParam("id") String id) {
    	final Optional<Session> conference = speakerRepository.findById(id);
    	return conference.orElseThrow(this::notFound);
	}

	@PUT
	@Path("{id}")
	public SessionDTO update(@PathParam("id") String id, @Valid SessionDTO sessionUpdated) {
    	final Optional<Session> optional = speakerRepository.findById(id);
    	final Session session = optional.orElseThrow(() -> notFound());
    	session.update(Session.of(sessionUpdated));
    	speakerRepository.save(session);
    	return SessionDTO.of(session);
	}

	@DELETE
	@Path("{id}")
	public Response remove(@PathParam("id") String id) {
    	speakerRepository.deleteById(id);
    	return status(NO_CONTENT).build();
	}

	@POST
	public SessionDTO insert(@Valid SessionDTO session) {
    	session.setId(UUID.randomUUID().toString());
    	return SessionDTO.of(speakerRepository.save(Session.of(session)));
	}

	private WebApplicationException notFound() {
    	return new WebApplicationException(Response.Status.NOT_FOUND);
	}
}

Conference service

The Conference service is the most connected of the services because it needs to keep information from both speaker and session services. It uses Payara Micro and MongoDB with Jakarta NoSQL.

import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Convert;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;

import java.time.Year;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

@Entity
public class Conference {

	@Id
	@Convert(ObjectIdConverter.class)
	private String id;

	@Column
	private String name;

	@Column
	private String city;

	@Column
	private String link;

	@Column
	@Convert(YearConverter.class)
	private Year year;

	@Column
	private List<Speaker> speakers;

	@Column
	private List<Session> sessions;

}

@Entity
public class Session {

	@Column
	private String id;

	@Column
	private String name;
}

@Entity
public class Speaker {

	@Column
	private Integer id;

	@Column
	private String name;

}

One of the benefits of MongoDB is that we can use the subdocument instead of creating a relationship. Therefore, we can embed Speaker and Session instead of doing joins. Note, the Speaker and Session entities have brief information such as name and ID.

import jakarta.nosql.mapping.Repository;

import java.util.List;

public interface ConferenceRepository extends Repository<Conference, String> {

	List<Conference> findAll();
}


import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.status;

@Path("conferences")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public class ConferenceResource {

	@Inject
	private ConferenceRepository conferenceRepository;

	@GET
	public List<ConferenceDTO> findAll() {
    	return conferenceRepository.findAll().stream()
            	.map(ConferenceDTO::of)
            	.collect(Collectors.toList());
	}

	@GET
	@Path("{id}")
	public ConferenceDTO findById(@PathParam("id") String id) {
    	final Optional<Conference> conference = conferenceRepository.findById(id);
    	return conference.map(ConferenceDTO::of).orElseThrow(this::notFound);
	}

	@PUT
	@Path("{id}")
	public ConferenceDTO update(@PathParam("id") String id, @Valid ConferenceDTO conferenceUpdated) {
    	final Optional<Conference> optional = conferenceRepository.findById(id);
    	final Conference conference = optional.orElseThrow(() -> notFound());
    	conference.update(Conference.of(conferenceUpdated));
    	conferenceRepository.save(conference);
    	return ConferenceDTO.of(conference);
	}

	@DELETE
	@Path("{id}")
	public Response remove(@PathParam("id") String id) {
    	conferenceRepository.deleteById(id);
    	return status(NO_CONTENT).build();
	}

	@POST
	public ConferenceDTO insert(@Valid ConferenceDTO conference) {
    	return ConferenceDTO.of(conferenceRepository.save(Conference.of(conference)));
	}

	private WebApplicationException notFound() {
    	return new WebApplicationException(Response.Status.NOT_FOUND);
	}
}

Client service

The client will show the RESTful application using HTML 5 with Eclipse Krazo. Yes, Eclipse Krazo has several engine extensions to use more than JSP, such as HTML 5. The extension we’ll use is Thymeleaf with Apache TomEE.

	<dependencies>
    	<dependency>
        	<groupId>org.eclipse.microprofile</groupId>
        	<artifactId>microprofile</artifactId>
        	<type>pom</type>
    	</dependency>
    	<dependency>
        	<groupId>sh.platform</groupId>
        	<artifactId>config</artifactId>
    	</dependency>
    	<dependency>
        	<groupId>org.eclipse.krazo</groupId>
        	<artifactId>krazo-core</artifactId>
        	<version>${version.krazo}</version>
    	</dependency>
    	<dependency>
        	<groupId>org.eclipse.krazo</groupId>
        	<artifactId>krazo-cxf</artifactId>
        	<version>${version.krazo}</version>
    	</dependency>
    	<dependency>
        	<groupId>javax.servlet</groupId>
        	<artifactId>jstl</artifactId>
        	<version>${jstl.version}</version>
    	</dependency>
    	<dependency>
        	<groupId>org.eclipse.krazo.ext</groupId>
        	<artifactId>krazo-thymeleaf</artifactId>
        	<version>${version.krazo}</version>
    	</dependency>
	</dependencies>

The first step in the client is to create the bridge to request information from the services. Thankfully, we have an Eclipse MicroProfile Rest Client to deal just with interfaces and nothing more.

package org.jespanol.client.speaker;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;

@Path("speakers")
@RegisterRestClient
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public interface SpeakerService {

	@GET
	List<Speaker> findAll();

	@GET
	@Path("{id}")
	Speaker findById(@PathParam("id") Integer id);

	@PUT
	@Path("{id}")
	Speaker update(@PathParam("id") Integer id, Speaker speaker);

	@DELETE
	@Path("{id}")
	Response remove(@PathParam("id") Integer id);

	@POST
	Speaker insert(Speaker speaker);

}


import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;

@Path("sessions")
@RegisterRestClient
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public interface SessionService {

	@GET
	List<Session> findAll(@QueryParam("search") String search);

	@GET
	List<Session> findAll();

	@GET
	@Path("{id}")
	Session findById(@PathParam("id") String id);

	@PUT
	@Path("{id}")
	Session update(@PathParam("id") String id, Session session);

	@DELETE
	@Path("{id}")
	Response remove(@PathParam("id") String id);

	@POST
	Session insert(Session session);

}


import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;

@Path("conferences")
@RegisterRestClient
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public interface ConferenceService {

	@GET
	List<Conference> findAll();

	@GET
	@Path("{id}")
	Conference findById(@PathParam("id") String id);

	@PUT
	@Path("{id}")
	Conference update(@PathParam("id") String id, Conference conference);

	@DELETE
	@Path("{id}")
	Response remove(@PathParam("id") String id);

	@POST
	Conference insert(Conference conference);
}

To make sure that the services are up, we conduct an Eclipse Microprofile Health check, so we can evaluate the HTTP status and the response time in milliseconds.

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;

import javax.ws.rs.client.Client;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

abstract class AbstractHealthCheck implements HealthCheck {


	abstract Client getClient();

	abstract String getUrl();

	abstract String getServiceName();

	@Override
	public HealthCheckResponse call() {
    	try {
        	long start = System.currentTimeMillis();
        	Response response = getClient().target(getUrl()).request(MediaType.TEXT_PLAIN_TYPE)
                	.get();
        	long end = System.currentTimeMillis() - start;
        	return HealthCheckResponse.named(getServiceName())
                	.withData("service", "available")
                	.withData("time millis", end)
                	.withData("status", response.getStatus())
                	.withData("status", response.getStatusInfo().toEnum().toString())
                	.up()
                	.build();
    	} catch (Exception exp) {
        	return HealthCheckResponse.named(getServiceName())
                	.withData("services", "not available")
                	.down()
                	.build();
    	}
	}
}

@Health
@ApplicationScoped
public class ConferenceHealthCheck extends AbstractHealthCheck {

	@Inject
	@ConfigProperty(name = "org.jespanol.client.conference.ConferenceService/mp-rest/url")
	private String url;

	private Client client;

	@PostConstruct
	public void init() {
    	this.client = ClientBuilder.newClient();
	}


	@Override
	Client getClient() {
    	return client;
	}

	@Override
	String getUrl() {
    	return url;
	}

	@Override
	String getServiceName() {
    	return "Conference Service";
	}
}


@Health
@ApplicationScoped
public class SessionHealthCheck extends AbstractHealthCheck {

	@Inject
	@ConfigProperty(name = "org.jespanol.client.session.SessionService/mp-rest/url")
	private String url;

	private Client client;

	@PostConstruct
	public void init() {
    	this.client = ClientBuilder.newClient();
	}


	@Override
	Client getClient() {
    	return client;
	}

	@Override
	String getUrl() {
    	return url;
	}

	@Override
	String getServiceName() {
    	return "Session Service";
	}
}


@Health
@ApplicationScoped
public class SpeakerHealthCheck extends AbstractHealthCheck {

	@Inject
	@ConfigProperty(name = "org.jespanol.client.speaker.SpeakerService/mp-rest/url")
	private String url;

	private Client client;

	@PostConstruct
	public void init() {
    	this.client = ClientBuilder.newClient();
	}

	@Override
	Client getClient() {
    	return client;
	}

	@Override
	String getUrl() {
    	return url;
	}

	@Override
	String getServiceName() {
    	return "Speaker Service";
	}
}

We can access the status at https://server_ip/health.

{
   "checks":[
  	{
     	"data":{
        	"time millis":11,
        	"service":"available",
        	"status":"OK"
     	},
     	"name":"Speaker Service",
     	"state":"UP"
  	},
  	{
     	"data":{
        	"time millis":11,
        	"service":"available",
        	"status":"OK"
     	},
     	"name":"Conference Service",
     	"state":"UP"
  	},
  	{
     	"data":{
        	"time millis":10,
        	"service":"available",
        	"status":"OK"
     	},
     	"name":"Session Service",
     	"state":"UP"
  	}
   ],
   "outcome":"UP",
   "status":"UP"
}

Once Services and health check are ready, let’s go to the controllers. Eclipse Krazo is an API built on JAX-RS. Therefore, any Jakarta EE developer will feel at home when creating a Controller class.

import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jespanol.client.session.SessionService;
import org.jespanol.client.speaker.SpeakerService;

import javax.inject.Inject;
import javax.mvc.Controller;
import javax.mvc.Models;
import javax.mvc.View;
import javax.ws.rs.BeanParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import java.util.Optional;

@Controller
@Path("conference")
public class ConferenceController {
	@Inject
	private Models models;

	@Inject
	@RestClient
	private SessionService sessionService;

	@Inject
	@RestClient
	private ConferenceService conferenceService;

	@Inject
	@RestClient
	private SpeakerService speakerService;

	@GET
	@View("conference.html")
	public void home() {
    	this.models.put("conferences", conferenceService.findAll());
	}


	@Path("add")
	@GET
	@View("conference-add.html")
	public void add() {
    	this.models.put("conference", new Conference());
    	this.models.put("speakers", speakerService.findAll());
    	this.models.put("presentations", sessionService.findAll());
	}

	@Path("delete/{id}")
	@GET
	@View("conference.html")
	public void delete(@PathParam("id") String id) {
    	conferenceService.remove(id);
    	this.models.put("conferences", conferenceService.findAll());
	}

	@Path("edit/{id}")
	@GET
	@View("conference-add.html")
	public void edit(@PathParam("id") String id) {
    	final Conference conference = Optional.ofNullable(conferenceService.findById(id))
            	.orElse(new Conference());
    	this.models.put("conference", conference);
    	this.models.put("speakers", speakerService.findAll());
    	this.models.put("presentations", sessionService.findAll());
	}

	@Path("add")
	@POST
	@View("conference.html")
	public void add(@BeanParam Conference conference) {
    	conference.update(speakerService, sessionService);
    	if (conference.isIdEmpty()) {
        	conferenceService.insert(conference);
    	} else {
        	conferenceService.update(conference.getId(), conference);
    	}
    	this.models.put("conferences", conferenceService.findAll());
	}
}

The HTML5 resource and replacement are on Thymeleaf. We wrote about it in a previous post, Thymeleaf puts the Java instance information on the HTML5 template dynamically.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html>
<head>
	<title>Latin America Conf (Session)</title>
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<link rel="stylesheet" href="/css/bootstrap.min.css">
	<meta charset="UTF-8">
</head>
<body>
<div class="container">
	<h1>Conference</h1>
	<form th:action="@{/conference/add}"  method="post" accept-charset="UTF-8">
    	<input id="id" name="id" type="hidden" th:value="${conference.id}">
    	<div class="form-group">
        	<label for="conferenceName">Name</label>
        	<input type="text" class="form-control" th:value="${conference.name}" name="name" id="conferenceName" placeholder="Enter Session Name" required>
    	</div>
    	<div class="form-group">
        	<label for="conferenceCity">City</label>
        	<input type="text" class="form-control" th:value="${conference.city}" name="city" id="conferenceCity" placeholder="Enter Conference City" required>
    	</div>
    	<div class="form-group">
        	<label for="conferenceLink">Link</label>
        	<input type="url" class="form-control" th:value="${conference.link}" name="link" id="conferenceLink" placeholder="Enter Conference Link" required>
    	</div>
    	<div class="form-group">
        	<label for="conferenceYear">Year</label>
        	<input type="number" class="form-control" th:value="${conference.year}" name="year" id="conferenceYear" placeholder="Enter Conference Year" required>
    	</div>
    	<div class="form-group">
        	<label for="conferenceSpeakers">Speakers</label>
        	<select class="form-control"  id="conferenceSpeakers" th:value="${conference.speakersIds}" name="speakers" multiple>
            	<tr th:each="speaker : ${speakers}">
                	<option th:value="${speaker.id}" th:text="${speaker.name}" th:selected="${conference.speakersIds.contains(speaker.id)}"></option>
            	</tr>
        	</select>
    	</div>
    	<div class="form-group">
        	<label for="conferenceSpeakers">Sessions</label>
        	<select class="form-control" id="conferenceSessions" th:value="${conference.sessionsIds}" name="presentations" multiple>
            	<tr th:each="presentation : ${presentations}">
                	<option th:value="${presentation.id}" th:text="${presentation.name}" th:selected="${conference.sessionsIds.contains(presentation.id)}"></option>
            	</tr>
        	</select>
    	</div>
    	<button type="submit" class="btn">Save</button>
	</form>
</div>
<script src="https://code.jquery.com/jquery.js"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>

Finally, the code

In this post, we finally saw some code and design, which is always exciting! Eclipse MicroProfile has a bright future integrated with Jakarta EE to allow Java developers to create several applications styles—like microservices and monoliths—that use either JPA or NoSQL. Besides that, these projects offer scalability and straightforward Java applications for your business. Platform.sh delivers on the promise of a NoOps pipeline for your Java microservices.