It can feel daunting to build a new microservice. It feels like there are a lot of things to keep in mind. Fortunately, Spring has a variety of libraries that can get us started quickly. We can focus on the bits that matter to us and let Spring scaffold the rest. In this post, we're going to take a look at what makes microservices different from other types of applications and how Spring helps us get up and running fast.
What Do We Mean by "Microservice"?
So, what exactly do we mean when we talk about microservices? They have their origins in a very specific type of service. It's not just any deployment artifact. In Martin Fowler's article about microservices, he mentions a few key characteristics that separate microservices from just another deployed app:
- Componentization via services.
- Organized around business capabilities.
- Products, not projects.
- Decentralized data management.
- Design for failure.
We're going to take a look at which Spring libraries help us achieve these characteristics. But before that, let's talk about how we can easily set up a new microservice with any set of libraries.
Spring Initializr, Our Launchpad
You're about to find out just how overwhelming the amount of Spring libraries that exist for building microservices is. Fortunately, there are two great tools to help us in our path. The first is Spring Initializr. This little site will get you up and running with a new Spring project in minutes, along with all the components you want to use.
The second tool is the website Baeldung, which is chock-full of in-depth Spring tutorials on all of the libraries we'll be using. You can use this site to dive deeper to any library for interest. They also have open-source code examples for their tutorials, from which we will be borrowing.
Now, onto the libraries.
Componentization Via Services
Componentizing into services is the idea that a microservice is independently deployable and runnable. In this vein, let's look at libraries that help us start up our application.
Spring Boot
Let's start with the foundation of everything: Spring Boot. This library is the basis of almost every other Spring library out there. Spring Boot sets up our application context, wiring up all our software components It also makes it really easy to execute our JAR—our software package—as a console application.
To include Spring Boot in your project, use Spring Initializr or add the following:
buildscript { ext { //This is the most recent version at the time of writing. springBootVersion = '2.1.1.RELEASE' } dependencies { //This makes the jar executable. classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } ... apply plugin: 'org.springframework.boot' //This makes it easier to manage the correct versions of the Spring libraries. The bill of materials ensures the versions are compatible with each other. apply plugin: 'io.spring.dependency-management' ... repositories { mavenCentral() //Some of the libraries we discuss are stored in the milestones repository. maven { url "https://repo.spring.io/milestone" } } ... dependencies { //This let's our code sping up a Spring ApplicationContext in our Main method implementation('org.springframework.boot:spring-boot-starter') //This gives us some unit testing utilities and runners testImplementation('org.springframework.boot:spring-boot-starter-test') } ... dependencyManagement { imports { //This is the actual bill of materials for spring dependencies mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" } }
For the examples in this post, I'll be using Gradle for dependency management, but you can also use Maven. Spring Initializr supports both.
With our dependencies in place, our application main method can look like this:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
And that's all you need to get started! Note, however, that there's a lot more functionality built into Spring Boot than this; explore it at your leisure.
Organized Around Business Capabilities
Microservices should be aligned only with business concepts like ordering, fulfillment, shipping, and customer service. They shouldn't be centered around concepts like data access, authentication, or email. Spring doesn't directly help with figuring out proper business boundaries. In fact, this is probably one of the hardest aspects of creating healthy microservices, and it's out of scope for this article. However, once you figure out your boundaries, Spring provides some libraries that will let you expose this functionality to your customers.
Web
Spring Web is a classic library that allows us to serve up both web pages and HTTP endpoints to our users. It also spins up an embedded Tomcat web server and binds to a port, so we can talk to the wider world.
The web dependency will look like this:
dependencies { //This can replace the "spring-boot-starter" dependency from above. implementation('org.springframework.boot:spring-boot-starter-web') ... }
With that in place, we can build HTTP endpoints:
@Controller public class SimpleController { @GetMapping("api/hello") public String homePage() { return "hello"; } }
If I GET /hello with curl or Postman, I'll see a response with "hello" as the only content.
There are loads more capabilities with Spring Boot Web. What I have shown here just barely scratches the surface. You'll have the ability to add security, deal with exceptions, add request/response middleware, and much more. Try out some tutorials on it.
Alternatives
Spring has a newer variant of building web apps called Spring WebFlux. This variant aims to be more reactive and to more easily support asynchronous, scalable operations.
Products, Not Projects
Products over projects is the spirit of DevOps. You build it, you run it. You can't just deploy a microservice into the ether—you have to monitor it and maintain it. Spring has a couple of libraries that can help us not only build but also run our microservices.
Actuator
You set it up like so:
dependencies { implementation('org.springframework.boot:spring-boot-starter-actuator') ... }
By default, only the /actuator/info and /health endpoints are enabled. You can enable all endpoints in your property file with:
management.endpoints.web.exposure.include=*
There are many endpoints in Actuator, and I recommend exploring them all. You can also see them through the /actuator endpoint. My favorite is /actuator/metrics. Its response looks something like this:
{ "names" : [ "jvm.memory.max", "jvm.memory.used", "jvm.memory.committed", "jvm.buffer.memory.used", "jvm.buffer.count", "jvm.buffer.total.capacity" ] }
If you drill into a specific one, such as /actuator/metrics/jvm.memory.max, you can see something like this:
{ "name" : "jvm.memory.max", "description" : "The maximum amount of memory in bytes that can be used for memory management", "baseUnit" : "bytes", "measurements" : [ { "statistic" : "VALUE", "value" : 2.384986111E9 } ], "availableTags" : [ { "tag" : "area", "values" : [ "heap", "nonheap" ] }, { "tag" : "id", "values" : [ "Compressed Class Space", "PS Survivor Space", "PS Old Gen", "Metaspace", "PS Eden Space", "Code Cache" ] } ] }
This immediately gives you a level of insight into your latency, error rates, and so on. You can also customize existing or new actuator endpoints as you desire.
Sleuth
It's highly likely that our microservices aren't running in isolation. At the end of the day, they have to communicate with queues, databases, and even other microservices to do their job fully. When things go wrong, it can be hard to track all the work that has happened in a request. If I want to be able to quickly debug issues across multiple deployed services, I need some tooling. Spring Sleuth lets us trace these requests across microservice boundaries. It can even let us trace to database calls.
We add it like so:
dependencies { implementation('org.springframework.cloud:spring-cloud-starter-sleuth') ... }
After this, we technically don't need to wire up any more code. It works with other Spring libraries to add tracing context when calling other services and databases. You can see this context when you log:
2018-01-10 22:36:38.254 INFO [Microservice Starter,4e30f7340b3fb631,4e30f7340b3fb631,false] 12516 --- [nio-8080-exec-1] c.b.spring.session.SleuthController : Hello Sleuth
The first GUID is the trace ID, which is the same across the request. The next GUID is the span ID, which represents the current unit of work. Having this context lets us query and group our log messages in a way that lets us see the life cycle of a request. You can also report these traces to external storage, but that is outside the scope of this article. Read this article by Baeldung for more information.
Decentralized Data Management
A microservice should own its data through and through and have minimal coupling to another service's data. No one else should be able to access its data directly.
JPA With SQL Server
Spring makes it easy for a microservice to own its own data for multiple data stores. Using the Spring JPA library lets us use Hibernate and the JPA specification to interact with a relational database like SQL Server.
You can wire it up like this:
dependencies { implementation('org.springframework.boot:spring-boot-starter-data-jpa') runtimeOnly('com.microsoft.sqlserver:mssql-jdbc') ... }
You then enable it in your application or an @Configuration:
@EnableJpaRepositories("org.scalyr.persistence.repo") @EntityScan("org.scalyr.persistence.model") @SpringBootApplication public class Application { ... }
Then we can map our classes to database tables:
@Entity public class Book { @id @GeneratedValue(strategy = GenerationType.AUTO) private long id; @Column(nullable = false, unique = true) private String title; @Column(nullable = false) private String author; }
and use Spring repositories to work with the data:
public interface BookRepository extends CrudRepository<Book, Long> { List<Book> findByTitle(String title); }
For more information, check out this tutorial.
Cloud Stream With Rabbit
Decentralizing data is a powerful way to keep microservices autonomous, but it's inevitable that some of this data will need to be shared across services. We don't want to share our databases, and we want to avoid runtime coupling on other services when possible. After all, we can't count on those services always being up and running. We can have our cake and eat it, too, by sharing data through event-driven messaging. Spring Cloud Stream with RabbitMQ makes this relatively easy to do.
We can add the dependencies as so:
dependencies { implementation('org.springframework.cloud:spring-cloud-starter-stream-rabbit') testImplementation('org.springframework.cloud:spring-cloud-stream-test-support') }
Then we can publish and subscribe to messages through Rabbit queues in our application:
@SpringBootApplication @EnableBinding(Processor.class) public class MyLoggerServiceApplication { public static void main(String[] args) { SpringApplication.run(MyLoggerServiceApplication.class, args); } @StreamListener(Processor.INPUT) @SendTo(Processor.OUTPUT) public LogMessage enrichLogMessage(LogMessage log) { return new LogMessage(String.format("[1]: %s", log.getMessage())); } }
INPUT and OUTPUT are built-in channels that let us specify from where we subscribe to messages and to where we publish them. We need to bind Rabbit to these channels:
spring: cloud: stream: bindings: input: destination: queue.log.messages binder: local_rabbit group: logMessageConsumers output: destination: queue.pretty.log.messages binder: local_rabbit binders: local_rabbit: type: rabbit environment: spring: rabbitmq: host: localhost port: 5672 username: guest password: guest virtual-host: / server: port: 0 management: health: binders: enabled: true
The application code remains blissfully ignorant of the specific transportation being used to publish or receive messages—we push all of that to the above configuration. You can see that we bound the INPUT and OUTPUT channels to specific exchanges in Rabbit. These exchanges and queues are automatically declared for us by the Cloud Stream Rabbit library. For more information, check out this article.
Alternatives
Spring also has support for NoSQL databases, like MongoDB. Spring supports just about any popular persistence mechanism. If using Cloud Stream support, we can use Kafka instead of RabbitMQ.
Design for Failure
When dealing with distributed, autonomous services, we can't count on them being up at all times. When communicating with other services, we should be ready to handle the inevitable.
Hystrix
Netflix built a library called Hystrix that lets us apply the circuit breaker pattern when communicating with other services. Using circuit breakers when communicating externally gives us a measure of resiliency to system outages. We can fall back to a default behavior when the service with which we want to communicate is unavailable.
The dependency is:
dependencies { implementation('org.springframework.cloud:spring-cloud-starter-netflix-hystrix') }
The configuration is dead simple:
@HystrixCommand( commandKey = "ratingsByIdFromDB", fallbackMethod = "findCachedRatingById", ignoreExceptions = { RatingNotFoundException.class }) public Rating findRatingById(Long ratingId) { return Optional.ofNullable(ratingRepository.findOne(ratingId)) .orElseThrow(() -> new RatingNotFoundException("Rating not found. ID: " + ratingId)); } public Rating findCachedRatingById(Long ratingId) { return cacheRepository.findCachedRatingById(ratingId); }
You can see that if the repository call fails, we can use a cached version. Feel free to read more here.
Retry
In many cases, circuit breaking may be a bit of overkill. Often we have intermittent network failures that we can overcome with a simple retry. Enter Spring Retry.
We add it to our Gradle build with:
dependencies { implementation('org.springframework.retry:spring-retry')) ... }
We then enable it via:
@Configuration @EnableRetry public class AppConfig { ... }
And we implement it with:
@Service public interface MyService { @Retryable( value = { SQLException.class }, maxAttempts = 2, backoff = @Backoff(delay = 5000)) void retryService(String sql) throws SQLException; ... }
This says, "Please retry 'retryService' up to two times if you see an SQLException, and wait 5,000 ms between each retry."
... And Many More
As you can see, Spring provides a myriad of libraries we can use to get a large boost into building microservices. We have much of what we need, from providing APIs to accessing data and even monitoring our application once it's in production. There are many more libraries you can use for more advanced use cases, so go out and explore using Spring for your own microservices.
Want to read more about Spring? We covered it in our "getting started quickly with logging" series, so head there next!