Design Scalable Java Microservices: Domain Modeling, REST APIs & Versioning
This article explains how to identify, design, and implement Java microservices—from domain‑driven modeling and service boundaries to RESTful API creation, documentation, proper HTTP verbs, and versioning strategies—using examples like an online retail store and the Game On! text adventure.
Prospects of Building Applications from Microservices
Creating applications composed of microservices raises questions for all languages: how large should a microservice be, what is the meaning of a focused service in a traditionally centralized governance model, and how should microservices handle conventional data modeling?
Example Applications
Two sample applications are used to illustrate the concepts.
Online Retail Store
The online retail store is a common example when discussing application architecture. The following microservices together form the application that lets users browse products and place orders:
Catalog service – information about products for sale.
Account service – data for each store account.
Payment service – processes payments.
Order service – manages the current order list and tasks required during the order lifecycle.
Game On!
Game On! is a scalable, text‑based adventure game built with microservices. Unlike the online store, it is a real application whose code is available on GitHub and whose architecture is described in a GitBook. The game consists of an infinite two‑dimensional network of interconnected rooms.
Rooms are provided by third parties (e.g., developers at conferences). Core services give a consistent user experience: users log in, issue simple text commands to interact with rooms, and move between them. Both synchronous and asynchronous communication patterns can be applied, and interactions between players, core services, and third‑party services are protected.
Java Platform and Programming Model
New Java applications should target at least Java 8 and use modern language features such as lambdas, parallel streams, and annotations to reduce boilerplate while balancing code cleanliness and maintainability.
To build fully functional microservices, low‑level Java libraries are required, but it is usually advisable to adopt a framework that provides sensible support for cross‑cutting concerns.
Spring Boot
Spring Boot offers a microservice creation mechanism based on opinionated technology choices. Spring Boot applications can be packaged as traditional WAR files for an application server or as executable JARs that embed a server (typically Tomcat). It emphasizes convention over configuration, using annotations and class‑path discovery to add functionality.
Spring Cloud provides integrations between third‑party cloud technologies and the Spring programming model.
Dropwizard
Dropwizard is another Java microservice framework that bundles a minimal set of technologies with an embedded Jetty servlet container. It offers strong support for application metrics and can be extended via community plugins.
Java Platform, Enterprise Edition (Java EE)
Java EE is an open, community‑driven standard for building enterprise applications. Its RESTful API (JAX‑RS) and Contexts and Dependency Injection (CDI) specifications are annotation‑driven and support lightweight protocols and asynchronous interaction patterns. Modern Java EE servers (WebSphere Liberty, WildFly, TomEE, Payara) generate immutable, self‑contained artifacts during automated builds, allowing easy migration between environments without code changes.
The examples in this chapter primarily use Java EE technologies such as JAX‑RS, CDI, and JPA.
Versioned Dependencies
Build tools like Apache Maven or Gradle provide simple mechanisms for defining and managing dependency versions, ensuring that artifacts behave consistently across production and test environments.
Tools that scaffold Maven/Gradle projects (WebSphere Liberty App Accelerator, Spring Initializr, WildFly Swarm Project Generator) help generate the necessary build artifacts.
Identifying Services
A microservice should be small in terms of its functional scope, not its code size. It should do one thing and do it well. Domain‑Driven Design (DDD) concepts—entities, value objects, aggregates, and services—help decide how to split an application into independent services.
Applying DDD Principles
In DDD, a domain is a specific area of activity. A model abstracts the important aspects of that domain and facilitates cross‑team communication.
For example, the Catalog service focuses on product details, while the Order service focuses on invoices and uses only minimal product information.
Bounded contexts allow each sub‑system to own its own model. When two services represent the same concept differently, context mapping and a shared language help coordinate information exchange.
Common Language in Game On!
Game On! uses three core services: Player (handles player data), an initial Room placeholder, and Concierge (later replaced by a Map service that records room locations). The Mediator service, derived from Player, mediates WebSocket connections between clients and room services.
Mapping Domain Elements to Services
Use value objects as parameters and return types, turning aggregates and entities into independent microservices.
Match domain services (operations not attached to aggregates) with separate microservices.
Each microservice should handle a complete business capability.
Player and Map Services
The Player service provides a resource API for managing player entities, including operations to generate usernames, favorite colors, and update positions.
The Map service provides a resource API for managing site entities, allowing developers to register room services. Its data representation differs from that used outside the service because some values are computed based on a site's position in the map.
Internal Structure of a Microservice
Organize code to separate concerns for easier testing. External service calls should be isolated from domain logic, which should also be separated from data‑marshalling support services.
The simplified internal architecture follows the Hexagonal (Ports and Adapters) pattern:
Resources expose JAX‑RS endpoints to external clients, performing basic validation before delegating to the domain layer.
Domain logic (entities, aggregates, services) implements business rules.
Repositories (optional) abstract data‑store interactions, enabling easy replacement of storage implementations.
Service connectors act as anti‑corruption layers, shielding domain logic from external API changes and handling protocol translation.
Classes should belong to one of four responsibilities: execute domain logic, expose resources, call external services, or call data stores.
Shared Libraries vs. Independent Services
Applying the DRY principle can lead to shared libraries, but excessive sharing may re‑introduce tight coupling. Options include accepting some redundancy, packaging shared code in a versioned library, or creating an independent service.
Shared libraries are useful for common algorithms (e.g., request signing in Game On!), but when a library starts requiring its own supporting services, it is often better to evolve it into an independent service.
Creating REST APIs
A good microservice API should be well‑designed, documented, and versioned. Follow standard HTTP verbs (POST, GET, PUT, PATCH, DELETE) and ensure idempotency where appropriate.
API Documentation
Tools such as OpenAPI/Swagger generate machine‑readable API specifications that can be used for client generation and contract testing. IBM WebSphere Liberty’s apiDiscovery feature automatically scans JAX‑RS annotations to produce Swagger definitions.
Choosing HTTP Verbs
Use POST for creating resources (non‑idempotent), GET for retrieving (idempotent, no side effects), PUT for full updates (idempotent), PATCH for partial updates (may be idempotent), and DELETE for removal (idempotent).
Resource URIs and Versioning
Resources should be nouns, pluralized, and follow a simple hierarchy. Versioning can be handled in three ways:
Embedding the version in the URI (e.g., /api/v1/accounts).
Using a custom request header.
Including the version in the Accept header.
Embedding the version in the URI is the simplest and works well with tools like Swagger and curl.
Example: Version in @ApplicationPath
package retail_store.accounts.api;
@ApplicationPath("/v1")
public class ApplicationClass extends Application {}Example: Version in @Path
package retail_store.accounts.api.v1;
@Path("v1/accounts")
public class AccountsAPI {
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response getAccounts(@PathParam("id") String id) {
getId(id);
// ...
}
}Conclusion
The chapter covered how to create Java microservices and use them. The next part will explore the evolution of microservice architectures. Happy learning!
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ITFLY8 Architecture Home
ITFLY8 Architecture Home - focused on architecture knowledge sharing and exchange, covering project management and product design. Includes large-scale distributed website architecture (high performance, high availability, caching, message queues...), design patterns, architecture patterns, big data, project management (SCRUM, PMP, Prince2), product design, and more.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
