Building Microservices with Spring and Cloud Spanner
Develop and test your Spring applications with Google Cloud Spanner
Google Cloud Spanner is a globally distributed relational database service. Using this solution, you can execute ACID transactions and use SQL semantics without compromising horizontal scaling and high availability.
In this article, we will go through how to develop and test a Spring Boot Kotlin application using Spanner (for free).
Why choosing Spanner?
If you are familiar with CAP theorem, you may know that any distributed data store can provide only two of the following three guarantees:
- Consistency: Every read must return the most recent write .
- Availability: Every read/write must have zero-error response.
- Partition Tolerance: The system continues to operate despite delays or communication errors between nodes (network partitions).
ACID databases, such as Traditional Relational Databases, are Consistent and Available, but not Partition Tolerant.
BASE databases, like many NoSQL Databases, are Available and Partition Tolerant, but not Consistent.
Cloud Spanner’s private network makes it possible to provide Consistency and Availability, thus minimizing the chances of network partitioning (Partition Tolerance).
However, because partitions are possible, Spanner usually prioritises Consistency over Availability.
Spanner Emulator
Spanner Emulator is an in-memory database that you can use to develop and test your applications for free.
Install and run the emulator following these instructions:
- Make sure Docker is installed.
- Get the latest emulator image.
$ docker pull gcr.io/cloud-spanner-emulator/emulator
3. Start the emulator.
$ docker run -d --name spanner-emulator -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
Once the emulator is running, you will have two endpoints available: localhost:9010
for gRPC communication, and localhost:9020
for REST.
Instances and Databases in Spanner
To use Spanner, you must create a Spanner instance. Instances provide resources to Spanner databases within these instances.
Create an instance and the database following these instructions:
- Make sure to have gcloud CLI installed.
- Create and activate the emulator configuration.
$ gcloud config configurations create emulator
$ gcloud config set auth/disable_credentials true
3. Set the configuration for your project.
$ gcloud config set project your-project-id
4. Override the endpoint from Spanner to localhost:9020
.
$ gcloud config set api_endpoint_overrides/spanner http://localhost:9020/
5. Create an instance with the emulator configuration and one node.
$ gcloud spanner instances create test-instance \
--description="Test Instance" --config=emulator-config --nodes=1
6. Create a database within your instance.
$ gcloud spanner databases create test-database \
--instance test-instance
7. Check if the database has status READY.
$ gcloud spanner databases list --instance test-instance
Setting up the application
The Dependencies
First, add the required dependencies for the Spring Boot application. Make sure you have the dependencies listed below in your build.gradle.kt
file.
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.google.cloud:google-cloud-spanner:$spannerVersion")
implementation("com.google.cloud:google-cloud-spanner-jdbc:$spannerJdbcVersion")
implementation("com.google.cloud:google-cloud-spanner-hibernate-dialect:$hibernateDialectVersion")
implementation("com.google.cloudspannerecosystem:liquibase-spanner:$liquibaseVersion")
The Migration Tool
In order to create the database schema when the microservices are deployed, we are using Liquibase. With the Spanner Liquibase Extension, you can enable Liquibase to target Spanner.
In the path src/main/resources
create a directory called db
and within it, another directory called changelog
.
Then, create a file called db.changelog-master.yaml
. In this Changelog file, we will define Liquibase’s “changeSets”.
Spring will read the Database Changelog file from the location classpath:/db/changelog/db.changelog-master.yaml
when the application is launched.
In this example, we are using YAML to design the schema following the Schema Design Best Practices for Spanner. In the db.changelog-master.yaml
file, we have the table singer
with the columns singer_id
and singer_name
:
databaseChangeLog:
- preConditions:
onFail: HALT
onError: HALT
- changeSet:
id: create-singers-table
author: spanner-examples
changes:
- createTable:
tableName: singer
columns:
- column:
name: singer_id
type: STRING(36)
constraints:
primaryKey: true
- column:
name: singer_name
type: STRING(255)
constraints:
nullable: false
The Datasource
Although Cloud Spanner has its integration with Spring Framework, we are using JDBC to connect the application with the Database. Set up the Datasource with the following properties:
your-project-id=test-spanner
your-instance=test-instance
your-database=test-database
# spanner datasource
spring.datasource.driver-class-name=com.google.cloud.spanner.jdbc.JdbcDriver
spring.jpa.properties.hibernate.dialect=com.google.cloud.spanner.hibernate.SpannerDialect
spring.datasource.url=jdbc:cloudspanner:/\
projects/${your-project-id}/\
instances/${your-instance}/\
databases/${your-database}?\
autoConfigEmulator=true
The Model
In the domain model, there is simply an Entity for the table singer
and its JPA Repository:
@Entity
@Table(name = "singer")
class Singer(
@Id @GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
@Column(name = "singer_id") val id: String?,
@Column(name = "singer_name") val name: String = ""
)
interface SingerRepo : JpaRepository<Singer, String>
Testing the Application
For the integration tests, we are using Testcontainers for Spanner. This library allows to easily create the Spanner instance and database in a Docker container.
First, make sure you have the following test dependencies in your build.gradle.kt
file.
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.testcontainers:testcontainers:$testcontainersVersion")
testImplementation("org.testcontainers:gcloud:$testcontainersVersion")
testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion")
In this example, we use a Companion Object that sets up the container with the Spanner Emulator, and creates the Spanner instance and the database for your tests:
Use the @Container
annotation to create a Docker container with the container image version. We recommend you to stick with a specific image version for your tests, instead of latest
.
Also, use the @DynamicPropertySource
annotation to override the spring.datasource.url
property with the URL pointing to the container.
Finally, we are creating a simple JUnit5 test, which injects the JPA Repository SingerRepo
to store and fetch the data from the database.
Summary
In this article, we learned how to build and test your microservices using a cloud database solution that manages distributed data, provides data consistency and is also highly available, like Google Cloud Spanner.
Thanks for reading. I hope this was helpful!
The example code is available on GitHub.