Spring Boot vs ASP.NET Core: A Showdown

Putu Prema
9 min readOct 4, 2021

--

I love Spring Boot and ASP.NET Core. Both are popular web frameworks among enterprises depending on which language they prefer. Those who prefer C# will use ASP.NET Core. For JVM-based language such as Java or Kotlin, Spring Boot is the most popular. Each has its own unique set of quirks that I really like.

In this post, we will find out how both frameworks differ in terms of the following aspects:

  • Controller
  • Model Binding & Validation
  • Exception Handling
  • Data Access (Repository and ORM)
  • Dependency Injection
  • Authentication & Authorization
  • Performance

Base Project

The base project for this comparison is a very simple backend API for product ordering application. Customers can create an order to purchase one or more products. I use MySQL for the database, and below is the ERD.

Entity Relationship Diagram (ERD)

I use latest version of Spring Boot (v2.5.5), and .NET 6 (which is still in RC preview). Especially for the Spring Boot project, I use Kotlin, my personal JVM-based language of choice. Without further ado, let’s get into the comparison.

Part 1: Controller

Controller is a layer responsible for handling incoming requests. To define a controller in Spring Boot, we create a class annotated with @RestController and followed by @RequestMapping for specifying the base path. On each controller methods, we can use one of the following annotations to define the supported HTTP method and (optionally) the path:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

To bind to a path variable, we can add parameter(s) to the controller method annotated with @PathVariable and specify the route path template with the same name as the parameter. Take a look at the getOrderById() method below where we bind id as the path variable.

In ASP.NET Core, it’s quite similar. To define a controller, we create a class that extends to ControllerBase class, annotated with [ApiController] and followed by [Route] attribute to specify the base path. On each controller methods, we can use one of the following attributes to define the supported HTTP method and (optionally) the path:

  • [HttpGet]
  • [HttpPost]
  • [HttpPut]
  • [HttpDelete]
  • [HttpPatch]

To bind to a route path segment, we can add parameter(s) to the controller method specify the route path template with the same name as the parameter. Take a look at the GetOrderById() method below where we bind id as the path variable.

One neat feature in ASP.NET Core is that you can have a route with the same name as the controller class or methods without declaring it explicitly using a so called token replacement. For example if you declare your route template like this:

The Test controller will have base path of v1/Test, and the SayHello method will have route of v1/Test/SayHello. Notice you don’t have to explicitly declare the name in the route template. [controller] will simply be replaced by the controller class name (excluding the “Controller” word), and [action] will be replaced by the method name.

Part 2: Model Binding & Validation

Model binding retrieves data from various sources of the incoming request and converts it to language-specific types so it can be used in the business logic. The sources can be a query string, request body, form data, or request headers. To do model binding in ASP.NET Core, we can apply one of the following attributes depending on the source to a parameter in our controller method:

  • [FromQuery] → Binds from query string
  • [FromRoute] → Binds from route data
  • [FromForm] → Binds from form data
  • [FromBody] → Binds from request body
  • [FromHeader] → Binds from request headers

What about model validation? Simple, just use attributes provided by System.ComponentModel.DataAnnotations package, apply it to your model fields and you are good to go. Examples are [Required], [MinLength], [MaxLength], and many more.

In Spring Boot, we do the same thing, just with different annotations:

  • @RequestParam → Binds from query string, only applies to single parameter
  • @RequestBody → Binds from request body
  • @RequestHeader → Bind from request headers

We don’t need annotations to bind from query string and form data.

And to do validation, annotate your DTO on the controller method parameter with @Valid to enforce validation (see register method on customer controller above), and use annotations such as @NotEmpty, @Length, and more. You need to add spring-boot-starter-validation dependency to your project beforehand.

Part 3: Exception Handling

Exception handling is one example of cross-cutting concerns that is not part of core business logic. Ideally, we should have a way to centralize this handling logic, so there are no repeated try-catch blocks scattered across the controller methods. Luckily, both frameworks have the ability to do just that.

In Spring Boot, we could create a class that extends ResponseEntityExceptionHandler class, annotated with @RestControllerAdvice. Inside, we add one method to handle each exception. Annotate the method with @ExceptionHandler and specify the exception class you want to handle. For example below I have created custom abstract exception class called AppException that has getResponse method to return the appropriate response to client.

In ASP.NET Core, exception handler is registered as a filter/middleware. Similarly, we create a class that implements IExceptionFilter interface, and implement the OnException method. There you can check which exception class is thrown and handle the response appropriately by setting a value on context.Result

Don’t forget to register the filter inside your Program.cs file.

Part 4: Data Access (Repository and ORM)

In ASP.NET Core, we can use an ORM provided by Microsoft called Entity Framework. First, we need to create a DB Context class. This is a class used by the ORM framework to connect to database and run queries. Inside this context class you declare one property of type DbSet<T>, each for the models. They will be used in our repository to perform query.

Next, we registered the DB context class above and define connection string to connect to database inside Program.cs

Inside our repository, we access DbSet fields in our db context class to perform queries. Here, we use LINQ (Language Integrated Query), a set of APIs baked directly into the C# language to do query from various data sources. This is one feature I really like, because it provides fluent APIs such as Where(), Include(), or OrderBy() that makes query much more natural.

Meanwhile in Spring Boot, we can use ORM called Hibernate, wrapped in the official Spring Data JPA starter project (you can add spring-boot-starter-data-jpa to your project dependency). There are no concept of DB Context, instead we just create a repository interface extending one of the built in repository interfaces such as JpaRepository, then you can have a basic query methods out of the box such as findAll() and findById().

You can also define custom query methods inside the interface. As long as you follow strict method naming convention, Spring will build implementation of this repository, including all the queries on runtime. Magic? Yes. That why I love Spring Boot very much. Or if you want to be more advanced, you can have custom JPQL queries (a query language similar to SQL) by annotating your query method with @Query. See example below.

What is @EntityGraph syntax you have there? Well, it is a helper feature in Spring Data JPA to prevent N+1 problem by joining entity relations in single query. It does not do that by default, unlike Entity Framework. It is however a more advanced feature that I won’t discuss in detail here. Read the official documentation if you want to know more.

Part 5: Dependency Injection

Dependency injection in Spring Boot is very simple, you just annotate a class with annotations such as @Component, @Service, or @Repository depending on the role of the class. On startup, it will do classpath scanning to pickup those annotated classes, and registers it for dependency injection.

In ASP.NET Core, by default you have to manually register the classes for dependency injection. Depending on the scope of the class, you can use one of the following methods to register the interface and its implementation class inside Program.cs file:

  • AddSingleton<Interface, Implementation>()
    Creates new instance of the class for the lifetime of the application.
  • AddScoped<Interface, Implementation>()
    Creates new instance of the class for the lifetime of a single request.
  • AddTransient<Interface, Implementation>()
    Creates new instance of the class every time it is requested by other class.

Part 6: Authentication & Authorization

Implementing JWT authentication and authorization is pretty simple in ASP.NET Core. You can install Microsoft.AspNetCore.Authentication.JwtBearer NuGet package. After that, configure some settings such as secret key, issuer, and expiration in Program.cs file.

To enforce the authentication, apply the [Authorize] attribute on either the controller level or controller method level. The rest of the heavy lifting will handled for you.

If you apply the attribute on controller level, you can also use [AllowAnonymous] attribute on certain methods to allow any access only to those methods.

Login endpoint is excluded from authentication

In Spring Boot, the security configuration is more verbose. First, you need to add spring-boot-starter-security dependency to your project. Additionally, add the following dependencies for the JWT library in your build.gradle file (or pom.xml if you use Maven):

implementation("io.jsonwebtoken:jjwt-api:${jjwtVersion}")
implementation("io.jsonwebtoken:jjwt-impl:${jjwtVersion}")
implementation("io.jsonwebtoken:jjwt-jackson:${jjwtVersion}")

Next, you will have to create a filter/middleware responsible for JWT token parsing and validation. It is a class that extends the OncePerRequestFilter class. Override the doFilterInternal method and put the parsing and validation logic there.

To configure and enforce the authentication, create a configuration class that extends WebSecurityConfigurerAdapter class, annotated with @Configuration. Here you register the JWT filter we created above, and configure which endpoints should have authentication inside the configure method. For example below, I allow anonymous access for customer login and register endpoints. Everything else should be authenticated.

You can also optionally register an authentication entry point to handle unauthenticated users, and access denied handler to handle unauthorized users.

You can also implement custom security logic in ASP.NET Core using the similar filter concept. But we won’t cover it here.

Part 7: Performance

Here comes the last but interesting part, performance. How good both frameworks perform in terms of request per second and RAM usage?

Here I did a load test calling an API that gets one product order by the id. As described in the ERD above, Product Order entity has many Order Items, and belongs to one Customer. So it will perform joins to get those relationship.

Execution Environment and Test Setup

CPU: Intel Core i7–8750H (up to 4.10 GHz) with 6 cores and 12 threads
RAM: 32 GB
Operating System: Windows 11

The load testing tool I used is k6 (https://k6.io). The test is conducted twice, because I want to measure how much the performance improves after the application has warmed up. On each test, the first 30 seconds will ramp up from 0 to 1000 virtual users, then stays there for 1 minute. Then for another 30 seconds the test will ramp down from 1000 to 0 user. I also capture the maximum RAM usage.

I also add Golang (with Gin framework and Gorm) to the benchmark. We all know that Golang is extremely fast. I just want to know how much different Spring Boot and ASP.NET Core performances are compared to Golang.

Results

Obviously, Golang is the fastest by a large margin. I don’t expect Spring Boot to come last. I checked that both performs optimized join and doesn’t have N+1 problem on the query. So ASP.NET Core wins in this case.

In RAM usage department, Golang is of course the smallest (only 113 MB!), followed by ASP.NET Core and the largest being Spring Boot with over 1 gigabytes of RAM. The interesting thing I observed is that after the test is finished, both Golang and ASP.NET Core memory consumption decreases to around 10 MB and 100 MB respectively, while Spring Boot stays over 1 GB until I kill the process.

Conclusion

Both frameworks have its own set of features that I really like. Although Spring Boot lose in the performance department, it doesn’t mean the framework is bad and you shouldn’t use it. Both Spring Boot and ASP.NET Core are very mature frameworks and keeps adding new features when new version is released. At the end, the best framework to use really depends on the use case of a project.

Since they are quite similar in terms of how we code stuff, I think it won’t be hard to learn ASP.NET Core if you are coming from Java/Kotlin background, and vice versa. I recommend you to learn both frameworks and see which one is good for your use cases.

Thank you for reading. Happy coding!

Source Code

Source code for the framework projects mentioned in this post can be found here:

--

--

Putu Prema

Currently focusing in fullstack development. My goal is to develop applications that make lives of millions of people easier.