There are many programming languages and frameworks that can be used to build microservices. However, when developing a particular microservices solution, you may discover that not every development tool can meet your needs. Choosing a technology stack is not an easy task, as your business requirements can limit your choice of technologies.
One of our recent projects at Apriorit was developing a microservices-based solution. Considering our client’s requirements, our choice of programming languages for microservices development was limited.
In this article, we compare the three programming languages we considered for this project: C#, Java, and Golang. We tested each of these microservices programming languages in different conditions to see which would work best for our client's project. Discover the pros and cons of each of these languages for developing a microservices-based solution.
Microservices refers to a type of service-oriented architecture (SOA) that allows you to develop software by joining together discrete services. These services are highly maintainable, loosely coupled, and can be deployed independently.
Netflix uses microservices to provide its popular video streaming service worldwide. Microservices are also used by global companies such as Amazon, Apple, and Uber.
Let’s see what advantages make microservice-based solutions so popular:
- Compatibility. Microservices can be developed with different programming languages, frameworks, and even hardware without affecting their compatibility.
- Flexibility. A microservice architecture provides flexibility to software, which means you can always expand a microservices-based solution with new functionality and deploy new services independently from existing ones.
- Simplicity. Microservices are simple, as they’re organized around business capabilities. Since each service is a separate code block with its own database, testing and development is easier and less time-consuming.
- Reliability. In case one service stops working (due to maintenance or malfunction), its temporary unavailability won’t affect the work of the rest of the system.
As we already mentioned, you can use various frameworks and programming languages to develop microservices. However, the actual development process entirely depends on your requirements, which will define what best suits your solution. Deciding which programming language to use is one of the most important steps in the development process.
In one of our recent projects, we worked on a microservices-based solution that needed to meet the following requirements:
- High processing performance. The microservices-based solution had to work as fast as possible with databases and routing.
- Minimum number of third-party libraries. The majority of third-party libraries use licenses that aren't suitable for enterprise solutions. In addition, even though these libraries provide robust functionality, using them may be unsafe in terms of cybersecurity.
- Comply with automation culture. Development and testing processes should be automated as much as possible to reduce the time required for software implementation and to mitigate the risk of human error.
- Component decentralization. Each service should be decentralized in order to speed up software development and simplify maintenance.
- Easy-to-find support. The system should be designed so it’s easy to find specialists who can support it.
- Support a culture of continuous integration and deployment. This allows for deploying and updating the product as fast as possible with the minimum expenditures. CI/CD culture also makes development faster and easier.
After considering these criteria, we chose C#, Java, and Golang as the best candidates. To determine which one was best suited to the project, we used each language to build microservices and tested our results to see the difference. To give you a better understanding of our choice, let’s start by explaining why these three programming languages can be used for developing microservices.
Before explaining which language we used in our project, let’s see how hard is it to learn and use each of these languages.
Java and C# are middle of the road in terms of their learning curve and convenience. To get along with them, you need to learn their operating principles, data structures, object-oriented programming (OOP), etc. Also, these languages have large communities and many books, guides, and tutorials, which makes them easier to learn.
There’s also a lot of syntactic sugar in each of these languages, which simplifies routine constructions (for instance, lambda expressions and anonymous classes). If something is missing in your software, you can easily create snippets or templates to generate code for typical tasks. For instance, for our client’s project, we wrote a template for typical CRUD functionality and generated all the layers: the user interface (UI), an application programming interface (API), service, and database.
The learning curve of Golang is extremely low. It’s said that it takes only two evenings of practice to get started with this programming language. If you already have experience developing in any popular programming language, then you don’t have to learn tons of new things to start practicing Golang. If you do have questions, you can find the full documentation here.
Since the Go programming language is simple, you won’t find popular features like inheritance; there’s only composition. There are also no numerations, only groups of constants. Additionally, Golang doesn’t support overloaded functions. For containers, there’s only slice arrays and named lists (maps). There are no exceptions, but Go provides an opportunity to return multiple values that contain the error objects. There are also no compilation warnings, only errors. Memory can be managed via the garbage collector or manually, using the unsafe package.
There is an encryption standard in Golang, compliance with which is ensured by formatting utilities, of which gofmt is the most popular. The IDE launches the formatting utility each time a file is saved, which means you don’t have to worry about the code style.
In Golang, there is native support for marshaling structures into JSON, BSON, and protocol buffer formats, which means you don’t have to create your own parsing functions. Also, in Go, copy-paste looks really unnatural because almost everything you need is present out of the box and almost 95% of the necessary functions you can implement directly from GitHub.
Rust vs C++ Comparison
To evaluate the performance of microservices based on each programming language we chose, we tested the following things:
- Speed of work with database and routing processing
- Simplicity and convenience of server creating
- Routing implementation
- Work with databases
- Work with third-party dependencies
- Convenience of utilities for writing code
The results of these tests show the differences in developing microservices using C#, Java, and Golang. Let’s compare them to define the most suitable programming language for our solution.
Our customer paid extra attention to the speed of routing processing and database access. Therefore, we decided to conduct database request tests in order to see precise numbers. To perform these tests, we prepared three identical servers using Java, C#, and Golang. Each server was packed into a Docker container to be able to balance its load in the future.
Here’s what the architecture of our testing application looked like:
Each server is represented as a REST application for manipulating the ToDoTask task. We chose MySQL as the database.
Creating a server with C#
Here’s an example of routing code for creating tasks using C# with the ASP.NET Core/Entity Framework:
Creating a server with Java
Now let’s see how you can create a task with Java using the Dropwizard framework:
Creating a server with Golang
Finally, here is an example of the task management code implemented with Golang:
When processing a single request, the response time difference of all the servers is almost unnoticeable, as you can see in the chart below. This is because the response time depends more on the connection quality and the system which hosts the server.
As it can be seen in the chart above, Java has the fastest processing time at 28 milliseconds, while Golang and C# took 33 and 40 milliseconds, respectively. However, if the application needs to process, say, 500 user requests, we’ll see a different picture (see Figure 3).
From the chart above, you can see that the response time increases with the number of requests. In this case, Golang has a huge advantage over C# and Java because it has the shortest response time during multiple requests. It took only 67 milliseconds for the Golang server to process 500 requests, which is 12 times faster than the C# server and about 4 times faster than the Java server.
The key reason for such a result from Golang is its parallel processing capabilities. Golang uses Goroutines, lightweight threads that have some advantages over regular operating system threads.
Let’s see what advantages Goroutines have:
- Stack size flexibility makes Goroutines much cheaper than operating system threads. The size of the stack can be increased or decreased depending on the application’s requirements, while operating system threads have a fixed size stack.
- Communication in Goroutines. This feature is implemented via channels. Thanks to the structure of these channels, conditions for data races are eliminated.
- Muxing. Goroutines are muxing into fewer operating system threads. This will offer an advantage if any Goroutine is blocked in the thread. In this case, another thread is created to which the rest of the Goroutines will be relocated. All these processes are performed under the hood, which gives you a pure API to work with parallelism.
In Java, the operating principle is the following: each request gets a new thread and blocks it until all functions within the thread are completed. Threads are combined to minimize the costs for invoking and killing processes. However, a large number of connections causes more united threads, which affects the system’s performance. Even though Java also has some turnkey functions to work with parallelism, this programming language still can’t solve the problems of applications with a large number of requests.
In C#, ASP.NET uses a regular multithreading model to handle requests, but this model is not beneficial compared to the model used in the other two programming languages. C# uses a standard thread pool, and it’s slower than programming languages that use special multithreading processing technologies. The Entity Framework also slows down the process, as it requires extra time for additional service requests when enabling/disabling the connection.
However, these are not all the features of the programming languages under test. After conducting performance tests, we also compared Golang, C#, and Java according to a number of important properties.
One of the most common tasks developers have to do is create a server, and this can be done in different ways. In order to see the difference in creating servers using different programming languages, let’s see some examples.
Creating a server in C#
With the C# programming language, you have a ready-to-use Kestrel server out of the box. However, Microsoft doesn’t recommend this server in a production environment and offers the following approach instead: create Nginx or Apache servers, tune the reverse proxy with configuration, and move all this to Kestrel. Why not use Kestrel in the first place? One of the reasons is poor functionality of the tuning process. Kestrel is a relatively new web server and is yet to be finalized. It can’t fulfill all tasks required in production.
Creating a server in Java
Creating a server with Java is not difficult due to the large number of frameworks and high-quality documentation. Here’s an example of how to create a server with the Spring framework:
Creating a server in Golang
In terms of creating a server, Golang is the most laconic and the simplest language. The following example shows that it takes just a dozen lines of code to make your server work:
This code example shows that among all tested programming languages, Golang is the most convenient, as it lets you create a server and run it in the shortest time. Now let’s see how C#, Java, and Golang do with routing.
Each of these programming languages implements routing differently, either built-in routing tools or via frameworks. Let’s see what each language can offer for tuning the routing.
Routing in C#
In C#, there’s functionality for routing with built-in tools and support for all required functions for deploying routes:
Routing in Java
The power of Java is in its libraries, as it has nothing to offer without them. For tuning routing, it’s necessary to use a framework. Dropwizard is a good choice of such a framework. Let’s see how to configure routing in Dropwizard:
Routing in Golang
This is how out-of-the-box routing looks in Golang:
The main disadvantage of native routing is that it doesn’t support any patterns or wildcard certificates for routes. In order to mitigate this drawback, developers often use the Mux library. Below is an example of how to use this library:
Again, among all the programming languages we tested, Golang is the most laconic and the simplest when it comes to routing. Now let’s take a look at how to use C#, Java, and Golang with databases.
For C#, standard work with databases is implemented in the Entity Framework. Its core functionality supports almost all you may need. The operating principle is the following: you create a class and it creates a mapping from a database. Here’s what working with a database looks in C#:
To work with databases, Java offers the Hibernate framework, where everything works based on bindings (creating a class where you can specify what data you need to obtain from a database and according to what filters). Thanks to such an approach, everything works simply and transparently. When it comes to complex requests (group requests to multiple tables, different JOINs), you need to write separate SQL requests.
In Golang, there is a standard database/SQL package that’s an interface for communication with multiple database management systems (DBMS) with drivers for them. All you need to do is include the driver package in a project, establish this dependency, and the DBMS will be ready to use. Here’s an example of code for connecting the PostgreSQL database:
Among these three programming languages, only Golang has packages for working with databases. The availability of these packages makes Golang more convenient than C# or Java in this regard. Let’s see what third-party dependencies you may need when working with C#, Java, and Golang during project development.
When building microservices, you may need to establish third-party dependencies for proper development. Let’s review what third-party components may be useful for each language.
Third-party components for C#
In order to develop a project in C#, you may need to have the .NET Command-line Interface (CLI), which allows you to build for Linux, macOS, and Windows operating systems. Let’s take a look at a properly configured file for establishing third-party dependencies:
After this, you can build your project using the following command:
Third-party components for Java
To build a project with Java, it’s usually enough to have the Maven or Gradle framework, which adds all required third-party components automatically. All you need to have is a properly configured file with all dependencies. Let’s create an example of this file using Gradle:
The following command is all you need for launching the project:
Third-party components for Golang
Golang is considered the most comfortable tool for working with microservices, as its standard library has all the required functionality. A need for external dependencies occurs only when you want to develop something extraordinary. Additional dependencies have to be included in the source file just like dependencies from the standard library. Here’s an example of how to establish dependencies using Golang:
Third-party library import sources need to be loaded to the common Golang folder $GOPATH/src. To load import sources, you can execute the following command:
You can use the dep tool to avoid loading each dependency manually:
This utility reads the file with dependencies from the Gopkg.toml project’s root. Then it reads the imports in the project code, defines revisions in repositories from which it has to download the code, and loads it to $GOPATH/src. Here’s an example:
This program, written in Golang, compiles into a single binary file. This eliminates the need to install additional frameworks to the target machine or include all dependencies in the program.
Besides tools for developing microservices-based solutions, let’s see what other tools there are for C#, Java, and Golang that you can use during the development process.
One of the most popular IDEs for Java is IntelliJ IDEA, as it’s cross-platform. This IDE supports many different tools to work with files, from Docker to JSON. Java also has many libraries for testing, so you can choose what’s right for your project. For unit testing, there’s the JUnit library, which is suitable for microservices.
The best IDE for Golang is Visual Studio Code because it’s cross-platform. For Golang, there are also many different plugins available. Visual Studio Code supports everything you may run into during microservices development (Docker files, BASH scripts, etc.) thanks to these plugins.
Golang supports testing and profiling out of the box thanks to the Go test utility. Depending on the test function statement, this utility can be a test or a benchmark function. Tests for packages are implemented in the package itself by adding _test.go to them. You can also implement black box tests (named using the convention <package>_test) in the other package.
In general, the test function accepts pointers to a structure from the standard testing package. The benchmark function is also considered as a kind of testing function, but it accepts another structure from the testing package, which is the main difference between it and the standard function. Here’s an example of how to implement both of these functions:
Golang encourages tests because test-driven development (TDD) is one of the best ways to develop microservices. Debugging microservices is inconvenient, which is why microservice solutions are usually based on tests and benchmarks.
Let’s summarize all of the above in order to determine which programming language is the most suitable for our project.
After testing these programming languages for microservice development, we defined their benefits and limitations.
Pros and cons of C#
Let’s start with С#. Our test results showed that this programming language has the following advantages:
- Cross-platform. C# uses .NET technology and the Common Language Runtime (CLR). This technology provides the opportunity to work with code written in Visual Basic .NET or other programming languages that support .NET.
- IDE convenience. C# works with Visual Studio IDE, which simplifies software development.
- Out-of-the-box utilities. Offering many turnkey solutions, .NET can boost the development of microservices-based products.
- Microsoft support. With the backing of Microsoft, C# will have long-term investments, which means this language will be evolving in the future.
- Quality documentation. MSDN has very good documentation, which helps developers use .NET technology in the most efficient way.
However, C# has one serious drawback you have to keep in mind:
- Windows-oriented. Although many cross-platform products for .NET development have already been released, there are still parts that work better on Windows (for instance, SignalR).
Pros and cons of Java
Now let’s see what Java has to offer:
- Long history. Java has 20+ years of history. During this period, the programming language has gained a huge community of many specialists.
- Wide range of utilities. Java has many libraries and frameworks that make development easy and comfortable.
Despite all the power of this programming language, however, it has two key limitations:
- Hard loading process
- High resource consumption during execution.
Pros and cons of Go
Finally, let’s overview the advantages of Golang:
- Wide choice of utilities. Golang offers a powerful standard library, lots of database drivers, out-of-the-box HTTP servers, serialization of data into different formats, and many other features that help you avoid third-party libraries.
- Fast compilation. Practically all Golang-based projects (regardless of their size) can be compiled within a minute.
- Easy to use. The Golang compiler generates only a single file with all the required dependencies, which means the executing file can easily be included in the container without additional settings.
- Easy Kubernetes integration. Nowadays, it’s hard to imagine writing microservices without using container orchestration tools. Any microservices solutions can be moved to a Docker container with orchestration tools like Kubernetes. However, Golang has a little bit of a bigger advantage compared to other programming languages. Since Kubernetes is developed with Golang, this gives you an opportunity to import cluster control utilities into your project.
- Code simplicity. Golang is considered a simple language, which makes it safer than other programming languages. The more complicated code is, the less secure it can be.
Golang has one main drawback, however:
- It’s a young programming language and all its components are open-source. This makes its libraries less secure compared to C#, for instance.
Based on our tests and knowledge of C#, Java, and Golang, we can see what these programming languages are capable of. Let’s recap what we saw when we ran these tests.
Creating microservices is tricky, and there are many factors that limit you in choosing the right technology stack for development. After testing C#, Java, and Golang in different conditions during microservices-based software development, we analyzed the differences in these programming languages and how those differences affect the overall development process.
C# is a good programming language for microservices development, as it has the Visual Studio IDE, is supported by Microsoft, and is suitable for developing software for Windows. Java is also a great choice for microservices because it’s cross-platform, has a large community (so it won’t be hard to find a specialist to work with), and has many libraries.
If such criteria as performance, server response, efficiency of deployment in the cloud, and simplicity of control have the highest priority, then consider using Golang for microservices development. In our case, this programming language is better suited than Java or C# for microservices-based solution development.