Comparing Backend Frameworks written in Java, Swift and Go
This article compares the backend frameworks Spring (Java), Vapor (Swift) and Gin (Go) in regards to ease of use, portability and flexibility. For that, I wrote the same RESTful micro-service using those three frameworks / languages. I will cover a bit of the languages’ look and feel, their development environment and show some sample code of handlers and ORMs. Last but not least, there will be a section on inversion of control in each of the frameworks. But first, let me give you a quick introduction…
I like Java…except for all the bulk it has collected in the past 24 years…and its lack of consistency. And then there is this JVM…don’t even get me started on that one. And finally, there are those frameworks…those frameworks that make me wonder wether I’m studying software engineering or wizardry (definitely not talking about OSGi here… #totallynoirony).
And still Java (with Spring) is the goto language for enterprise backend development. That’s no different here at comsysto.
For my part, except for educational tasks, I grew up writing everything from desktop-apps (not a very good idea) to web-servers (very good idea) in Go. Having learned to love the language’s clarity and its expressive style, the performance and lightweight dependencies, I was quite shocked when I was thrown back into the muddy waters of enterprise-style Java and its annotation-based frameworks.
Lately, I got a little into iOS development and thus started to learn Swift. Since it is a very flexible language, it can also be used for backend development, which enables you to share code between your backend and (iOS/macOS) frontend. Thus, I chose Swift as the third language for this comparison. (This is also due to Swift for Tensorflow being on the rise, which could be a way more flexible alternative to Tensorflow’s Java or Go APIs.)
tl;dr: There’s a quick recap of the whole article at the very bottom.
The languages: Java vs Swift vs Go
Let’s begin with the languages themselves and my experience working with them on the backend.
The keywords are my first measurement for this comparison, because I think their amount has a major influence on how a language feels:
Of course, having many keywords is not bad by definition, but definitely not my personal preference. On the one hand, an additional keyword often provides a more concise way of expressing something and it can provide more context to an operation.
On the other hand, if there are more keywords, there are more options of doing the same. And if there are more options of doing the same, code becomes less and less readable. I.e. reading code efficiently requires more and more experience. Opposed to that, if there are very few keywords (as in Go), it’s almost like there is only one canonical way of solving a specific problem. This helps a lot when reading someone else’s code and thus can improve the overall solidity of a project, even though the number of lines will rise a bit. And especially on the backend I think this is a very good trade.
So, Java and especially Swift provide more concise ways for expressing your logic. This is not only due to the additional amount of keywords, but also the fact, that they support generics, while Go doesn’t. Generics, however, enable handy tools like the Streaming-API, which can reduce the amount of code significantly. Furthermore, Java and Swift tend to be a bit more explicit about certain things, e.g. optionals. Swift has pretty much an extra syntax for handling them (consisting of ?
and !
) and Java has the generic Optional<T>
class. In Go, it’s common to just use a pointer and set it to nil
if the optional is empty.
Still, if you have worked with Go for a couple of months, you will often read through other people’s code and find exactly what you expected underneath a function’s signature, because you would have done it exactly the same way. That doesn’t sound like a lot, but in my experience, it’s a huge advantage you get from Go’s minimalistic and idiomatic design.
Compiler and Development-Environment
Well, obviously Swift and Go are compiled languages, whereas Java has the JVM. Does this make development harder with Swift or Go? No, definitely not. The compilers for Swift and especially Go are really fast and after the first build you probably won’t even notice a difference. All of them have dependency management systems (Gradle/Maven, Swift Package Manager, Go Modules) and starting a debugging session is pretty easy, too. You can work with all of them relying solely on open-source software. However, that’s where things start to drift apart:
For Go I’m happy with VS Code, its Go plugin plus the Go command-line tools. They really work perfectly together and I don’t miss anything GoLand or any other IDE could give me. With Java, however, it’s a different story. Sure, you can develop on the equivalent stack consisting of VS Code + Spring Tools + Java Plugin, but the paid IntelliJ Ultimate Edition provides a way more comfortable and streamlined experience. Of course, this is very subjective and your experience will heavily depend on your very own preferences.
By the way: working with Go or Java on Windows is no problem at all (except for you having to use Windows😉). With Swift, apparently you can, but I personally haven’t seen anybody doing it (You’d probably want to use the Linux Subsystem for Windows).
On Linux, working with Swift is not quite as comfortable as using XCode on macOS (as I did), but it’s definitely an option if you don’t mind using lldb on the command line for debugging (I’ve read there are also integrations for VS Code, Atom and CLion, but I don’t know how mature they are yet).
In case you’re wondering about cross-compiling in Go and Swift: With Go it’s dead simple; with Swift it is not.
With Go, all you have to do is setting two environment variables: GOOS
and GOARCH
. That’s all. For example, GOOS=windows GOARCH=386 go build .
would build your program for a 32-bit Windows machine. UseGOOS=js GOARCH=wasm go build .
and you get Web-Assembly to run in the browser. Basically, everything you could possibly want is supported: here is a list of all supported combinations.
With Swift, you have to rely on scripts like that one to set up a cross compilation toolchain or figure it out yourself…depending on the target system.
Just one more thing about compiler-output: If you are used to the incredibly helpful error-messages you get when working with Java or Go, Swift can be quite shocking. If your’e a C-developer, it’s not that much of a difference.
The Sample Project
Disclaimer
I’m a working student and thus do not claim to be anything close to a backend-expert. I am especially un-experienced in Swift, but I haven’t worked much with Spring ether. I hope I didn’t violate too many standards and best practices. Everything you see here does not represent the quality-standards present at Comsysto Reply. I conducted this experiment to gather experience for myself. However, I hope this series of articles can be of some value to others, even if they are way more experienced in one of those frameworks.
Before I start comparing the actual frameworks, let me give you a quick introduction to my sample project: It’s a JSON-based HTTP server for managing a pizza-service🍕. It is backed by a MySQL database and does not handle authentication or anything fancy, it is only responsible for managing recipes, and available resources/ingredients.
The API has the following endpoints, which work with the upper models recipe and resource:
- PUT
api/v1/recipe
: Creates a recipe from the given model and returns its id. - PUT
api/v1/recipe/:id
: Updates the recipe:id
to match the given model. - DELETE
api/v1/recipe/:id
: Deletes the recipe:id
from the database. - GET
api/v1/recipe
: List all recipes. - GET
api/v1/recipe/:id
: Return the recipe:id
. - POST
api/v1/refill
: Add the given resources to the default store. - GET
api/v1/store
: List all stored resources. - POST
api/v1/order/:id
: Order recipe:id
(remove the required resources from store or cancel order, in case the required ingredients are not in store).
When building this sample project in Spring Boot, Vapor and Gin, I tried to stick to two objectives: Try to use the framework’s default wherever possible and fulfill the following requirements:
- Implement the functionality listed above.
- Every unsuccessful response must contain a
message
field - Successful responses provide their content in the
data
field. They can also contain amessage
. - Stick to the same response-codes.
The Frameworks: Spring Boot vs Vapor vs Gin
Why those? They are the most popular ones fore each of the three languages. Spring Boot currently has 42k stars on Github, followed by Gin with 31.5k. The definite looser in this segment is Vapor with “only” 17k.
The docs are quite different. While Spring Boot provides many introductory as well as topic-specific guides and a solid Reference Documentation, Gin’s docs are more example-based and consist of about 45% advertisements, 5% text and 50% code. However, the 1:10 balance of the latter two isn’t that much of a problem, because most of the code and API is very clear and easy to use. Vapor’s documentation is more of one big getting-started tour, which is quite comfortable to use, but it could be a bit more extensive.
Also, you’ll find a lot more about Spring Boot on StackOverflow than about the other two frameworks.
Of course, Spring-Boot is also the most feature-complete of the competitors (I won’t go into detail here, because frankly I’ve never worked with anything that goes beyond what Viper or Gin provide). Anyways, there are good chances you’ll find what you need somewhere on Github:
Handlers
Let’s get into some code: the handlers for the create-recipe endpoint (PUT api/v1/recipe
).
So, with Spring, writing a handler is pretty straight forward. The method’s return-value and parameters are encoded/decoded automatically. For the parameters, we have to specify where we expect the value to be. Here, the recipe-model is inside the @RequestBody
. (For the update-recipe endpoint, we could access the id using the @PathVariable(value="id")
annotation.)
Handling errors is just a matter of throwing a ResponseStatusException
with the according HttpStatus and message. If the request is successful, you just return the value. I use my Response type for wrapping data and message, in order to get the desired format. However, if you want to specify another successful status code than OK, you have to make some modifications. There are some other ways to do it, but my favorite is to add a HttpServletResponse
parameter to the handler-method. It is provided by the Spring framework and has a setStatus
method. Afterwards you can just return the response-value as usual.
In Go, the handler’s input and output is not represented by the function’s header at all. Instead, everything happens inside the function’s body. What you get as a parameter is a pointer to a gin.Context
. This is essentially a wrapper for both request and response. It allows you to access and decode body and URL parameters using methods like BindJSON
, Param
or the more generous ShouldBind
versions.
To send a response, call the context’s JSON
(or whatever encoding you want) function with the status code and whatever you want to send and return. Gin provides a shortcut for the most important type: gin.H
: it is a map[string]interface{}
(For those who don’t know Go: An interface{}
is the empty interface. And since interface-implementation is implicit in Go, anything implements the empty interface). It also fits our message-data-format.
That’s also the default way of handling errors: check for the returned error
and encode your answer along with a status BadRequest or whatever you like.
For Swift and Vapor, everything gets a bit more complicated. Since there is no lightweight concurrency model yet for Swift, Vapor creates only one thread per CPU core. Each thread is assigned to a so called event loop, which is responsible for handling the requests. This is really great for performance, however, it results in a major problem for the programmer. If you wrote your code just like with the other two frameworks, i.e. in a synchronous manner, every blocking task would reduce the performance and latency of your application heavily. If one handler is waiting for your database to return whatever it requested, all the other thousands of requests on the same event loop can’t be processed for that exact same amount of time. That is, why we have to write asynchronous code for everything that goes beyond pure calculation…even throwing errors. Vapor’s take on this are generic Future
s and Promise
s.
So, let’s start again with the handler’s signature: you get a Request
, which allows you to access parameters and content. The function may throw (which makes error-handling a bit more eloquent than in Go) and it returns a Future<Wrapper<Int>>
. So…what’s that? In the center, we have an Int
, which is what we expect it to return (the recipe’s id). The Wrapper
is the equivalent to the generic Response
in my Java/Spring code; it creates the data-message format. The Future
is what makes the function asynchronous. It is created from a Promise
, which is done by the framework most of the time. The Future
is essentially just a reference. It is filled with content when the Promise
succeeds. Then, the operations you’ve set up for the Future
are executed on its content. This is expressed using the Future
‘s methods map
and flatMap
(there are more than that, but those two will be enough for now).
Back to the code: in line 2, we start out by trying to decode
the Recipe
we expect in the Request
‘s content
. This returns an EventLoopFuture<Recipe>
, which is just some specific type of future “containing” a Recipe
. Then, we’re processing this Recipe
in three steps starting at lines 2, 12 and 20.
In the first step, we get the recipe
from our call to req.content.decode
passed into our closure (that’s an inline-function in Swift). We want to map this recipe
to a tuple containing a DBRecipe?
(an optional of a recipe in the format I use for storing it in the database) and the recipe
we got passed into the closure. Before we construct the closure’s return-value, we can do some error checks on our parameter, the recipe
. I attached the status codes and messages to the error-constants in order to clean up the handler’s code a little.
In line 11, we query for any duplicate with the same title as our recipe
, which returns a Future<DBRecipe?>
. The Future
type provides the function and
to create a tuple, which is exactly what we want. Since stacked closures are not nice to read at all, we return at this point and continue with the second step, by calling flatMap
on the Future<(DBRecipe?, Recipe)>
our first closure returned. The second step includes asserting, that the duplicate is nil
, as well as saving the recipe
to the database.
For the third and last step, we use map
instead of flatMap
, since we actually return a Wrapper
from the closure and not a Future<Wrapper>
. Thus, there is nothing to be flattend.
I’m not sure, how familiar everyone is with futures, that’s why I got into more detail here. However, I think we can all agree, that this asynchronous style makes it more complicated to work with the framework and to implement or read your own logic.
Models and ORMs
On that point, I really had the best experience with Spring. For both of the other frameworks I had to create an internal and a public version of each model and write some code for converting them into each other.
Spring’s annotation-based style fits in very nicely with ORMs. I think the code is pretty self-explanatory. The @JsonIgnore
annotation allows you to hide fields from the public api. This enables us to use a single model class for the whole system.
Furthermore, there is the extremely powerful @JsonView
annotation. It enables you not only to have a private and public version, but also multiple public versions for different endpoints. I didn’t need this feature for my sample-project, but here’s a short tutorial from Baeldung in case you are interested.
The basic CRUD operations for your @Entity
class are provided by the generic CrudRepository
interface. The framework takes care of implementing it…
I had to provide code for storing the Recipe
nevertheless. At least I couldn’t figure out how to maintain the one-to-many relationship without any extra code.
In case you need some custom queries, that’s easy, too, if you know SQL. Just provide the query’s signature as a method-declaration along with the @Query
annotation, which contains your SQL code. You can reference the @Param
s inside your SQL code using a colon (as far as I know, that’s also injection-safe).
As stated before, I didn’t get around specifying separate structs for database and public model in Go. The gorm.Model
contains the model’s id and some timestamps. The default configuration worked perfectly well for me, therefore I didn’t even need a single annotation (you could add those by adding `gorm:"foreignkey:RecipeReference"`
or similar behind a field).
GORM also provides all the CRUD operations, and they worked pretty well even with the one-to-many relation between a recipe and its resources, however I found it best to wrap them in custom methods where I could do some of the assertions, in order to remove some complexity from the handlers.
To give you a quick impression on custom queries in Go, here’s an example from the create recipe handler which asserts, that our recipe
is unique.
In the first line, we declare a Recipe
. In the second line, we query for any recipe which’s title
is equal to recipe.Title
. We only need to know if one exists, therefore it is enough to get the First
. We provide a pointer to the previously declared duplicate
. In case a duplicate was found, it is written to that pointer. Thus, we can check, if the duplicate
‘s Title
is not empty, which implies, that a duplicate was found. In that case we respond with an error. Again, the ?
placeholder syntax takes care of injection-attacks, however, GORM doesn’t type-check your queries. I.e. with Spring Data, if I accidentally replaced r.title
by r.id
in the findByTitle
custom query, I’d get a status 500 whenever I use the method. With GORM, if I replaced title = ?
by id = ?
, there would be no error at all; duplicate.Title
would be unset all the time.
The ORM library for Vapor is called Fluent. The two most important things here are the implemented protocols (interfaces in Swift): MySQLModel
and Migration
. The first declares our class a Model
that is to be saved via the MySQL driver (in case we have multiple different databases). The second enables automatic migration during startup.
There’s nothing special about the fields id
and title
/name
. However, the one-to-many relation between a DBRecipe
and multiple DBResources
is rather interesting. For the parent side Fluent provides the Children
construct. We provide the keypath \.recipeID
to this construct. It refers to DBResource.recipeID
, which is where the parent is referenced in the child-class.
Keypaths and typealiases are also the tools to customize Fluent models. You add them as static constants to your model class. E.g. with typealias ID = UUID
you could change the model’s identifier-type. Adding the property static let idKey: IDKey = \.identifier
would cause Fluent to look for the id at DBRecipe.identifier
.
So, what about this `public`
stuff? That’s the code I had to write to convert the database models to the public representation (Recipe
and Resource
), which can be encoded to JSON.
The one thing, that’s really cool about Fluent is its custom queries. Thanks to keypaths and operator overloading, Fluent is able to provide a very intuitive syntax as a replacement for SQL and compile-time type-safety!
Managing environment-dependent Strategies
That’s the point where things get “magical” with Spring…
But let’s start out with the intended behavior in our sample project. I want to add an additional endpoint: GET api/v1/price/:id
, which calculates the price of the recipe with id :id
and returns the result in Euros. We have three strategies: in development environment, everything costs nothing. In production, we have a “fixed” and the “normal” strategy. In “fixed” mode, every recipe costs 7€. In normal mode, the price is calculated from the recipe’s ingredients.
Let’s start out with Go, because it’s really straight-forward.
The first step is the same for all three languages: defining a suiting interface/protocol. In this case, it’s called priceCalculator
. However, the key point here with Go is the init()
function. It can be declared for each package and is called at startup after all the dependencies’ init functions have been called and all the packages global variables/constants have been initialized. Inside of it, I’m just initializing the according implementation depending on the two environment variables "PRICE"
and "GIN_MODE"
(the latter one is provided by the Gin framework via gin.Mode()
.
Of course, if you’re building a serious application, you’d be better off using a package like viper in combination with the factory pattern described in this excellent article (It does basically the same, but it is more solid).
Vapor’s take on this is called Service
. Again, we start out by defining a protocol in line 3. All of our implementations must comply to the empty protocol Service
(line 21). The nasty thing here is, that all our implementations must comply to the asynchronous api, even if the simple ones wouldn’t need it.
The service is to be registered inside the central configure.swift
file. Firstly, we provide closures, that instantiate our implementations for a given container. We’ve done that in lines 31 to 40, where we’ve given the framework three viable options to use as a PriceCalculator
. Secondly, we have to specify which option is to be used in which case. That can be done by calling config.prefer
with the according types based on command-line flags and environment variables.
Now, we can use the Service in our controllers. We can get an instance of it from the framework by calling make
on our container
(the Request
). We only specify the protocol’s type; the implementation is chosen based on our calls to config.prefer
inside the configure
function.
With Spring, after defining our interface PriceCalculator
, it’s all about annotations. Each implementation is annotated with @Component
and @Profile
. The first basically is the same as the call to services.register
with Vapor. With @Profile("fixed")
we state, that the implementation may only be used if the profile "fixed"
is active. The name "default"
is reserved for our fallback-implementation. It is used whenever no other implementation for the same interface is available under the given configuration. The active profiles can be specified using environment variables, command line arguments and a config file.
How do we access those implementations inside a controller? Using another annotation, of course. The @Autowired
in line 44 signalizes the Spring framework, that it shall initialize the PriceCalculator
property below with whatever implementation was configured.
If you know Java, but no Spring, you have no idea what’s going on; if you know Spring, its just really convenient…
Recap
I personally like Go’s development environment best, but Java’s isn’t that bad ether. Swift’s inaccurate compiler errors can get really annoying and you’ll have some trouble when it comes to cross-compiling.
Documentation and community support is best for Spring/Java, then Gin/Go and worst for Vapor/Swift.
On the one hand, I love Go’s clarity and explicitness, because it makes it really easy to jump through code and get a deep understanding of how other packages work. Working with Go just feels like breathing fresh air after a long time in a hot and dusty room (Also, the language’s concurrency features are just awesome!). On the other hand, Spring’s annotation based style can save you quite some typing and is optimized for your daily tasks. It takes a while getting used to Vapor’s asynchronous style and it doesn’t help to make your code more readable.
The ORMs all have their benefits and drawbacks. I really don’t have a preference here.
Inversion of Control is easiest with Spring’s annotations. Vapor’s approach feels a bit overly complicated (probably due to the central registration/configuration), but it definitely does its job. With Go, it’s kind of up to you, how to solve this, but I think with viper and the article I referenced you’re on a good path to a clean and solid solution.
Overall, I think there’s no clear winner, but a clear looser: Swift + Vapor. I’m sorry, but I’m a bit disappointed here😕. Still, I hope you enjoyed reading this article!
References
Here is the full code of all three Implementations: Java (Spring), Swift (Vapor) and Go (Gin).
Passion, friendship, honesty, curiosity. If this appeals to you, Comsysto may well be your future. Apply now to join our team!
This blogpost is published by Comsysto Reply GmbH