Microservice Tech Stack at Rungway - Part 1
Starting this year the Engineering team at Rungway will be creating blog posts to discuss and demonstrate some of the technologies we have in use and our experiences. We are starting with a 3 part blog post series related to the development of microservices. In this first part we’ll write an overview of the different technology choices in our microservice infrastructure. The second blog post will be related to individual microservices and reasoning about creating new services vs changing existing ones. The third and final post will be on our testing methodology.
Background & Microservices
I won’t go into details about our product (you can go to rungway.com or linkedin.com/company/rungway to learn more about us) but at a high level we have a workplace advice platform where users are able to ask questions and voice issues anonymously or publicly, and efficiently share their experience with colleagues. It is available as a website and iOS/Android mobile applications communicating with a Kubernetes backend where we have a number of microservices running connecting to different data stores.
Most of our microservices are JVM based applications, primarily built with Spring Boot. Love it or hate it, Spring Boot is one of the easiest platforms to develop well-tested applications with immense extensibility available. We do however have a few non-JVM microservices (e.g. Python with Flask) but we only venture outside the JVM when there is a very specific reason to as we have invested time in building libraries and infrastructure around it.
Primary Development Language
Spring Boot lets you develop in many different programming languages, not just Java. Most of our current services are implemented in Java but we also have services implemented in Groovy and Kotlin.
- Java is the safe general-purpose language that everybody knows (and loves 😅) and you really can’t go wrong. Supported by all JVM application frameworks, it is a great language that (since Java 8) includes everything you need to produce good quality code. In my opinion though Lombok is an almost requirement when writing in Java to hide all that boilerplate code with
- Groovy is that laid back cousin of Java that barely resembles it! It has much less ceremony than Java and hence it’s easier to learn and use which then causes its main drawback: it is too easy to use so it lets you do things that you probably shouldn’t! Don’t get me wrong though, we at Rungway love Groovy but as a testing language in combination with Spock as you will see on part 3 of this series.
- Kotlin needs no introduction as it is highly popular these days as a replacement for Java. Kotlin is much more concise than Java (being a half functional language), gives you compile-time null-safety checking (its killer feature) plus tons of other features (my favourite: Delegated Properties). If Java has everything you need, then Kotlin has everything you need, want and didn’t even know you wanted!
Having a microservice infrastructure allows us to experiment with different things when building new services. Within the last year we’ve experimented with these 3 languages running on Spring Boot and right now for any new services we are tending to use Kotlin. I am keen to experiment with competing frameworks to Spring Boot such as Quarkus or Micronaut in the imminent future (they just didn’t seem fully featured or stable yet). Something else we’ll probably experiment with is GraphQL. All our services right now are REST based so this will be a more involved experiment as we’d need a buy-in from front-end developers as well since ultimately they’ll need to change the way they query the backend.
Kubernetes & Interservice Communication
In an microservice infrastructure, inter-service communication needs to happen. For synchronous communication we make use of OpenFeign via Spring Cloud. This library makes it simple to communicate with other REST endpoints without having to worry about connections, parsing JSON, etc. Just create a service interface (with a couple of feign annotations) and a resource object. That’s it!
For async communication (pub/sub) we use RabbitMq and leverage Spring’s AMQP library. Setting it up is “fairly” simple, it is annotation based but we needed an extra configuration class to do some setup such as setting up JSON converter and backoff policies. We use async communication in our event delivery system which would be a nice separate topic for discussion.
At Rungway I believe we have architected our microservices at a good level that is not too granular where you need to communicate with lots of other services and not too coarse where a service feels like they are doing too much. Right now we have over 20 microservices and on average a service needs to talk to one or two other services.
Our services all run on Kubernetes and there’s a little bit of configuration (couple of YAML lines) that needs to be done to tell Kubernetes to allow a service to talk to another or connect to a specific exchange or queue on RabbitMq. We use Spring Cloud Kubernetes libraries and those libraries transparently wire things up with Kubernetes so there’s nothing Kubernetes specific in code that needs to be done other than adding library dependencies.
Authentication & Authorization
As mentioned in our background section, our product handles client data that is anonymous and could potentially contain sensitive information. We take very seriously the importance of appropriately handling client data and therefore we and our clients hold ourselves to high standards. We have architected our infrastructure using “security by design” approach and have several layers of security which we’ll not go into here. In relation to this post, the fundamentals are that (nearly) all endpoints can only be accessed by authenticated users and only if they are authorised to access that endpoint. We use JWT to represent user entitlements and all users get a JWT token when they login which is passed between the different microservices that they use.
By using JWTs it makes it easy for services to extract data and work out user entitlements. It’s easy but not trivial however, so we built a well-tested library to handle anything related to auth and to hide from normal development any complicated logic. Our library also handles passing the JWT in service-to-service communication all transparently from the developer. In the end, all that the developer has to do in order to implement an authorization check is to write a single line of code (well, technically you also need 1 line to import that library). This was the goal from the beginning, the less code you write the less likely you are to have bugs. And a bug related to auth can be catastrophic so it is important to leave as little room to mess up as possible!
Now, at the same time, this 1 line of code is probably the most important 1 line of code in an endpoint and hence it has to have good testing around it. Again, we have built another testing library to make the developers life in testing as easy as possible. Developers tend to add more tests if writing a test is easy.
To Be Continued…
This concludes part 1 of this series. In part 2 we’ll talk about choices going into creating a new service vs changing existing service and part 3 we’ll talk about testing.