How to Write Microservices in Java

Explore microservices in other languages

Writing Microservices in Java

Microservices, the backbone of modern software architecture, are like the Swiss Army knife of software architecture. It doesn’t matter if you’re building the next big e-commerce platform or a startup around a single service, microservices have become the go-to solution for crafting scalable, fault-tolerant systems. 

The best part? 

They’re not exclusive to any particular programming language; you can wield the power of microservices in virtually any language.

Today, we’re diving right into the world of microservices, and our companion on this adventure is going to be Java. Java, with its age-old (nearly three decades!) reliability and a treasure trove of libraries and frameworks, stands as a very strong contender in the realm of microservices.

So, whether you’re an experienced developer looking to expand your skill set or a newcomer eager to explore the world of microservices, get ready because we’re going to cover all you need to get started. 

Let’s dive right in!

Pros and cons of building microservices with Java

Given Java is nearly 30 years old, by now we can all safely call it a very mature and evolved high-level language. Because of that, there are many benefits of using it to build microservices.

Mind you, like with any other language, it’s not perfect and there are some downsides to using it as well.

So let’s first take a look at the pros of using Java to build microservices:

  • Language Familiarity: Java is a popular and widely used programming language making it a natural choice for building microservices. Developers who are already proficient in Java can leverage their existing knowledge to create efficient microservices.

     

  • Robust Ecosystem: Java boasts a mature and robust ecosystem of libraries, frameworks, and tools. This rich ecosystem simplifies the development and maintenance of microservices, offering solutions for everything from REST APIs to asynchronous messaging.

     

  • Scalability: Microservices need to be scalable, and Java excels in this area. With Kubernetes and Docker, you can easily manage the scale of your Java-based microservices to meet variable demand.

     

  • Community Support: The Java community is vast and active. You can find abundant resources, documentation, and forums to help you troubleshoot issues and learn from others’ experiences.

All these benefits stem mainly from Java’s age and robustness as a language. Let’s now take a look at a couple of “cons”:

  • Resource Consumption: Java microservices can be resource-intensive due to the Java Virtual Machine (JVM). However, optimizations and efficient resource management can mitigate this drawback. Of course, going down that rabbit hole can turn into a project on its own, so be careful when picking Java as your language of choice, if the microservice is really “micro” and the set of features is quite reduced, it might not be the best choice for the job.

  • Complexity: Microservices architecture can be complex, and Java can introduce additional complexity due to its verbosity. Careful design and adherence to best practices are crucial. This is also a case of “careful with what you pick,” because the extra verbosity added by Java is great when the project is big enough, adding a lot of implicit documentation and IntelliSense, but if you’re just creating a service with two endpoints, you might end up writing more boilerplate code than actual business logic. That said, we should also mention that all the common IDEs used for JAVA (such as Eclipse, IntelliJ, Netbeans and others) will auto-generate all that boilerplate code for you. So as long as you know your IDE, then this is definitely a minor “con” (if at all).

Decoding Microservices: Best Practices Handbook for Developers

Learn how to overcome microservices challenges by following some best practices

Microservices in Java example

In this hands-on example, we’ll create a simplified product catalog microservice using Java and Spring Boot. This microservice will expose RESTful endpoints for managing product information.

Step 1: Project setup

Start by setting up a new Spring Boot project using your preferred build tool, such as Maven or Gradle. You can use Spring Initializr to generate a project with the necessary dependencies.

Step 2: Define the data model

Create a Java class to represent the product entity. Annotate it with Java Persistence API (JPA) annotations to map it to a database table:

				
					@Entity
public class Product {
    @Id
    @GeratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

				
			

Step 3: Implement RESTful endpoints

Create a controller class to define RESTful endpoints for managing products. The “Controller” is going to act as a capture logic for all our HTTP requests for specific URLs (our endpoints), and it’s going to redirect the program’s flow into the proper service class (which is where the actual business logic will reside).

				
					@RestController
@RequestMapping("/products")
public class ProductController {
    @Autowired
    private ProductService productService;

    @GetMapping
    public List<Product> getAllProducts() {
        return productService.getAllProducts();
    }

    @GetMapping("/{id}")
    public Product getProductById(@PathVariable Long id) {
        return productService.getProductById(id);
    }

    @PostMapping
    public Product addProduct(@RequestBody Product product) {
        return productService.addProduct(product);
    }

    @PutMapping("/{id}")
    public Product updateProduct(@PathVariable Long id, @RequestBody Product product) {
        return productService.updateProduct(id, product);
    }

    @DeleteMapping("/{id}")
    public void deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
    }
}

				
			

Step 4: Implement the service logic

The services contain the business logic which is the code that will be executed with this microservice.

As a side note: You can also see in this example the verbosity aspect mentioned before. We’ve implemented a data model, a controller, and now we’re implementing the actual business logic.

Leaving aside that comment, let’s create a service class to handle business logic and interact with the database. Here’s a simplified example:

				
					@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;

    public List<Product> getAllProducts() {
        return productRepository.findAll();
    }

    public Product getProductById(Long id) {
        return productRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Product not found with id " + id));
    }

    public Product addProduct(Product product) {
        return productRepository.save(product);
    }

    public Product updateProduct(Long id, Product product) {
        getProductById(id); // Check if the product exists
        product.setId(id);
        return productRepository.save(product);
    }

    public void deleteProduct(Long id) {
        getProductById(id); // Check if the product exists
        productRepository.deleteById(id);
    }
}

				
			

You’ll notice that we’re not directly accessing the database, instead, it is done through a repository class. This adds a layer of abstraction on top of the data-related code, which would allow us to change storage in the future without affecting our main business logic.

Step 5: Implement the repository class

The final class to implement is the repository, which can also be referenced as the “data layer,” since it acts as a layer between the logic and the data storage.

In a production environment, we would implement database access code here, but given that we’re just doing a simple example, the methods implemented here use an in-memory map instead.

This exemplifies the versatility added by the use of this pattern since we’re separating the data access code from the business logic. If you wanted to add SQL support for this code, you could do this without affecting any other class.

				
					import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Repository
public class FakeProductRepository {
    private final Map<Long, Product> productMap = new HashMap<>();
    private long nextProductId = 1;

    // Save a product to the in-memory storage
    public Product save(Product product) {
        if (product.getId() == null) {
            // If the product doesn't have an ID, assign one
            product.setId(nextProductId++);
        }
        productMap.put(product.getId(), product);
        return product;
    }

    // Retrieve a product by ID
    public Product findById(Long id) {
        return productMap.get(id);
    }

    // Retrieve all products
    public List<Product> findAll() {
        return new ArrayList<>(productMap.values());
    }

    // Delete a product by ID
    public void deleteById(Long id) {
        productMap.remove(id);
    }
}

				
			

That’s it! You’ve just created a basic product catalog microservice in Java using Spring Boot. You can expand upon this example, obviously, by adding more features like authentication, pagination, and validation to suit your specific requirements.

Orchestrating your microservices

Enhance your Java-based microservices with the robust orchestration capabilities of Camunda. 

With Java’s strong typing and object-oriented features, organizing complex systems often requires meticulous attention to detail. Camunda’s workflow engine integrates seamlessly into your Java environment, bringing the power of visual process modeling, automated state management, and advanced event handling. 

It translates BPMN models into actionable workflows that synergize with Java’s reliability, making it easier to collaborate across teams, scale operations, and maintain service resilience. 

By embracing Camunda within your Java microservices strategy, your organization benefits from a streamlined process lifecycle that ranges from design to improvement, ensuring that your services remain agile and aligned with your business objectives.

With Camunda being such a powerful workflow automation and decision-making platform that can be integrated into your microservices architecture, you can orchestrate complex workflows, manage business processes, and make data-driven decisions within your microservices ecosystem.

Curious to see how Camunda can help you with microservice orchestration? Check out this article about the top 6 benefits of setting up an event-driven process orchestration, and then dive right into Camunda with a free account.

Start orchestrating your microservices with Camunda