Week 2 | Lesson 6

Handling Business Logic

Authentication and Authorization
+
Anonymous Functions and Lambda Expressions, Scope Functions

Service Layer

Service Layer

Main responsibility of the service layer is to handle the business logic of the application.

Business logic is the part of the application that defines how the application behaves and what it does under certain conditions.

The service layer is often responsible for:

  • Executing the business logic of the application, enforcing business rules and constraints, such as ensuring valid state transitions, data integrity, and consistency.
  • Validating input data and ensuring it meets the requirements of the business logic.
  • Coordinating interactions between different components of the application, such as the data access layer, external APIs, and other services.
  • Mapping data between different representations, such as converting database entities to domain models or DTOs (Data Transfer Objects).
  • Performing complex calculations or data transformations that are part of the business logic.
  • Managing transactions and ensuring that changes to the data are consistent and atomic.
  • Handling errors and exceptions that may occur during the execution of the business logic.

Executing Business Logic

The business logic should not leak to other layers!

This way, the service layer can be tested independently of the data access layer and the presentation layer. The business logic can also be reused in different contexts, such as in a web application, a mobile application, or a command-line application.

Example:

						
							class OrderService(
								private val orderRepository: OrderRepository
							) {

								fun createOrder(request: CreateOrderRequest): CreateOrderResponse? {
									val orderItemIds = request.items.map { it.menuItemId }
									val price = when (request.customerId) {
										null -> orderPriceService.getPriceWithoutDiscount(itemIds = orderItemIds)
										else -> orderPriceService.getPriceWithDiscount(
											customerId = request.customerId,
											itemIds = orderItemIds
										)
									}

									val orderDto = OrderDTO(
										id = null, // ID will be generated by the database
										customerId = request.customerId,
										itemIds = orderItemIds,
										price = price,
										status = OrderStatus.PENDING_PAYMENT
									)

									val orderId = orderRepository.insertOrder(orderDto)

									return CreateOrderResponse(
										orderId = orderId,
										totalPrice = orderDto.price,
										status = orderDto.status
									)
								}
							}
						
					

Domain Models Mapping

To keep the separation of responsibilities between the application layers, the service layer can perform data mapping between different representations, such as converting database entities to domain models or DTOs (Data Transfer Objects) and vice versa.

Example:

						
							class MenuService(
								private val menuRepository: MenuRepository
							) {

								fun getMenuItems(filter: String?): Set<MenuItemResponse> {
									return menuRepository.getAllMenuItems(filter).map { item ->
										item.toResponse()
									}.toSet()
								}

								fun createMenuItem(request: MenuItemRequest): MenuItemResponse {
									return menuRepository.addMenuItem(request.toDTO()).toResponse()
								}

								private fun MenuItemRequest.toDTO(): MenuItemDTO {
									return MenuItemDTO(
										id = null, // ID will be generated by the repository
										name = name,
										description = description,
										price = price,
										isDeleted = false // New items are not deleted
									)
								}
								private fun MenuItemDTO.toResponse(): MenuItemResponse {
									return MenuItemResponse(
										id = requireNotNull(id) { "MenuItemDTO id cannot be null" },
										name = name,
										description = description,
										price = price
									)
								}
							}
						
					

Coordination and Transaction Management

In case the business logic consists of multiple steps that modify different resources, it is the responsibility of the service layer to ensure data integrity.

For example, creating an order may involve creating a new order entity, updating the inventory, and charging the customer.

The service layer should make sure that if one of the steps fails, the changes are rolled back and the system remains in a consistent state.

Authentication and Authorization

Authentication and Authorization

Authentication and authorization are two critical parts of securing applications and systems. They ensure that only authorized users can access resources and perform actions within the system.

Authentication


Is the process of verifying the identity of a user or system. It ensures that the user is who they claim to be.

  • Is usually done on the top level (routing) of an application
  • Can also be done by the infrastructure (proxy, API gateway, VPN)
  • Can be done using various methods, such as username and password, tokens, or certificates

Authorization


Is the process of granting or denying access to resources based on the authenticated user's permissions. It determines what actions a user can perform after they have been authenticated.

  • Is usually done on the business logic level
  • Can be done using various methods, such as role-based access control (RBAC), attribute-based access control (ABAC), or policy-based access control (PBAC)

Authentication

There are several methods of authentication, including username and password, token-based authentication, and OAuth.

Username and Password

The most common method of authentication is using a username and password. The user provides their credentials, which are then verified against a database or other storage.

Token-based Authentication

Token-based authentication is a more secure method of authentication that uses tokens to verify the user's identity. The user provides their credentials, which are then used to generate a token that is sent back to the client. The client then includes the token in subsequent requests to authenticate the user.

An example of token-based authentication is JSON Web Token (JWT).

JWT is a compact, URL-safe means of representing claims to be transferred between two parties. It allows you to verify the authenticity of the claims and the identity of the user.

Another token-based approach is API keys, which are often used to authenticate requests to APIs.

OAuth

OAuth is an open standard for access delegation that allows users to grant third-party applications access to their resources without sharing their credentials. It is commonly used for social media logins and other third-party integrations.

Basic Auth

Basic authentication is a simple authentication scheme built into the HTTP protocol.

It is based on a challenge-response mechanism that requires the client to send a username and password with each request.

  1. The client sends the credentials in the Authorization: Basic {username:password in Base64} header of the HTTP request.
  2. The server then verifies the credentials and grants access to the requested resource if they are valid.

Basic authentication is not secure by itself, as the credentials are sent in plain text. It is recommended to use it over HTTPS to encrypt the communication and protect the credentials from being intercepted.

JWT

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. It allows you to verify the authenticity of the claims and the identity of the user.

A JWT is composed of three parts:

  • Header: Contains the type of token (JWT) and the signing algorithm used (e.g., HMAC SHA256). Example: {"alg": "HS256", "typ": "JWT"}
  • Payload: Contains the claims, which can include user information, roles, and permissions. Example: {"sub": "1234567890", "name": "John Doe", "admin": true}
  • Signature: Created by combining the encoded header and payload with a secret key using the specified algorithm. This ensures that the token has not been altered.

Obtaining a JWT typically involves the following steps:

  1. The user provides their credentials (e.g., username and password) to the server.
  2. The server verifies the credentials and, if valid, generates a JWT containing the user's claims.
  3. The server sends the JWT back to the client, which stores it (e.g., in local storage or a cookie).
  4. The client includes the JWT in the Authorization: Bearer {JWT} header of later requests to authenticate the user.
JWTs can also be used to pass information between different services in a microservices architecture. They can be signed using a secret key or a public/private key pair, depending on the security requirements of the application.

API Tokens

API tokens are unique identifiers that are used to authenticate requests to APIs.

This mechanism is often used in service-to-service communication or when a user needs to access an API without providing their credentials directly.

The server generates an API token for the user or application, which is then included in the request headers or as a query parameter when making API calls.

The token is commonly sent in the X-API-Key header of the HTTP request, but it is up to the server to define how the token should be sent.

API tokens can be long-lived or short-lived, depending on the security requirements of the application. They can also be revoked or rotated to enhance security.

Authorization

The process of granting or denying access to resources based on the authenticated user's permissions.

The authorization mechanism is almost exclusively implemented in the service layer.

In case JWT is used, the authorization can be done by checking the claims in the token.

Example of a claim may be:

  • Role or permissions, which can be used to determine what actions the user is allowed to perform.
  • Organization or department, which can be used to restrict access to resources based on the user's affiliation.
  • Permissions are withing the organization, which can be used to restrict access to resources based on the user's role within the organization.
Kotlin Basics

Anonymous Function

Lambda Expression

Anonymous Function & Lambda Expression

Anonymous functions and lambda expressions are used to define functions without names.

Lambda Expression

Lambda expressions are typically used for short, concise functions that are passed as arguments to higher-order functions. They are commonly used in collection operations like , filter, and forEach.

						
							val lambdaName: (Type) -> ReturnType = { argument: Type -> body }
						
					
If lambda expression has a single parameter, you can use the default name it.

Anonymous Function

Anonymous functions are used when you need more control over the function's return type or when you need to use the return statement to exit the function itself rather than the enclosing function.

						
							val lambdaName = fun(name: Type): Type {
								return value
							}
						
					
Opposite of anonymous function is called a named function.

Lambda Expression

Examples

Function with one parameter and no return value:

						
							val greet: (String) -> Unit = { name -> println("Hello, $name!") }

							greet("World")
						
					

If lambda expression has a single parameter, you can use the default name it:

						
							val greet: (String) -> Unit = { println("Hello, $it!") }

							greet("World")
						
					

Function with two parameters and a return value:

						
							val multiply: (Int, Int) -> Int = { a, b -> a * b }

							val result = multiply(10, 20)
						
					

Common examples of lambda function is the forEach:

						
							val cities = listOf("Bangkok", "Barcelona", "Tokyo", "London", "New York")

							cities.forEach { city ->
								println(city)
							}
						
					

Anonymous Function

Examples
Anonymous function with one parameter and no return value:
							
								val greet = fun(name: String) {
									println("Hello, $name!")
								}

								greet("World")
							
						
Anonymous function with two parameters and a return value:
							
								val multiply = fun(a: Int, b: Int): Int {
									return a * b
								}

								val result = multiply(10, 20)
							
						

Anonymous Function & Lambda Expression

Usage

In summary, lambda expressions are more concise and are typically used for simpler functions, while anonymous functions provide more flexibility with explicit return types and return behavior.

There are few use cases for lambda expressions and anonymous functions:

  • Passing functions as arguments to higher-order functions
  • Returning functions from other functions
  • Defining local functions that are not needed outside the scope of the enclosing function

Anonymous Function & Lambda Expression

Usage

In this example, the operation function takes two integers and a lambda function as arguments.

						
							fun operation(x: Int, y: Int, func: (Int, Int) -> Int): Int {
								return func(x, y)
							}
						
					

The most common use way pass the function argument is:

						
							val result = operation(10, 20) { x, y -> x + y }
						
					

Another possibility is to pass a function reference (it can be a named function or a member function):

						
							val multiply: (Int, Int) -> Int = { a, b -> a * b }

							val result = operation(10, 20, multiply)
						
					

Anonymous Function & Lambda Expression

Usage

You can also return functions from other functions.

						
							fun getCalculator(): (Int, Long, Double) -> Double {
								return { a, b, c -> a + b + c }
							}
						
					
						
							val calculator = getCalculator()

							val result = calculator(1, 2, 3.0)
						
					

Exercise

Create a function named updateAtIndex that takes the following parameters:
  • An array of strings (Array<String>).
  • A variable number of integer indices ((vararg atIndex: Int).
  • A lambda function ((func: (String) -> String) that takes a string as an argument and returns a string.

The function should return a new array (copy) of Array<String> where the elements at the specified indices are updated using the provided lambda function. If any of the specified indices are out of bounds, the function should throw an error with the message "Index out of bounds".

Example

Given the following input:

  • array = ["a", "b", "c", "d", "e"]
  • atIndex: = 1, 3
  • func = { it.uppercase() }

The function should return:

["a", "B", "c", "D", "e"]

Extension Functions

Extension functions

Extension functions allow adding new methods to existing classes without modifying their source code.

Extension functions are one of the most powerful and popular features of Kotlin. You define an extension function by prefixing the function name with the type you want to extend.


Benefits of extension functions include:
  • Adding new functionality to existing classes which you may not have access to.
  • Using extension functions to create a more fluent API and DSLs.
  • Improving readability and maintainability of code by encapsulating and naming logic in extension functions.
  • Using extension functions to transform objects into other objects.

Extension functions: Examples

Adding new functionality to existing classes which you may not have access to.

For example, you can add a new method to the String class to capitalize the first letter of each word in a sentence.

						
							fun String.capitalizeWords(): String {
								return this.split(" ").joinToString(" ") { it.capitalize() }
							}
						
					

You can then use this extension function on any String object.

						
							fun main() {
								val sentence = "hello world from kotlin"
								println(sentence.capitalizeWords()) // Output: "Hello World From Kotlin"
							}
						
					

Extension functions: Examples

Using extension functions to create a more fluent API and DSLs.

You can use extension functions to create a more fluent API by adding methods to existing classes that allow you to chain method calls together.

						
							data class Person(
								val name: String,
								val age: Int,
								val country: String
							)
						
					
						
							fun List<Person>.filterByCountry(country: String): List<Person> {
								return this.filter { it.country.lowercase() == country.lowercase()) }
							}

							fun List<Person>.sortByName(): List<Person> {
								return this.sortedBy { person -> person.name }
							}
						
					

You can then use these extension functions to create a more readable and expressive code.

						
							val people = listOf(
								// list of people
							)

							val filteredPeople = people
								.filterByCountry("Thailand")
								.sortByName()
						
					

Extension functions: Examples

Improving readability and maintainability of code by encapsulating and naming logic in extension functions.
						
							data class Person(
								val name: String,
								val age: Int,
								val country: String
							)
						
					
						
							fun Person.canDrinkBeer(): Boolean {
								val legalAge = when (country) {
									"USA" -> 21
									else -> 18
								}
								return age >= legalAge
							}
						
					
						
							fun main() {
								val person = Person("John", 21, "USA")
								println("${person.name} can drink beer: ${person.canDrinkBeer()}")
							}
						
					

Extension functions: Examples

Using extension functions to transform objects into other objects.
						
							data class Person(
								val name: String,
								val age: Int,
								val country: String
							)
						
					
						
							data class Student(
								val name: String,
								val country: String,
								val dateEnrolled: LocalDate
							)
						
					
						
							fun Person.toStudent() = Student(
								name = name,
								country = country,
								dateEnrolled = LocalDate.now()
							)
						
					
						
							val person = Person("John", 21, "USA")
    						val student = person.toStudent()
						
					

Exercise

Implement the following extension functions ...

A) Int extension

Implement an extension functions isEven and isOdd for the Int class that returns Boolean.


B) Array extension

Create an extension function for the Array<String> class that will return a new array with the same elements repeated twice.

For example, if the input array is ["a", "b", "c"], the output array should be ["a", "b", "c", "a", "b", "c"].

Scope Functions

Scope Functions

Scope functions allow you to execute a block of code within the context of an object.

When you use a scope function, you can access the object's properties and functions without having to use the object's name.

The scope functions in Kotlin are let, run, with, apply, and also.

Each scope function has a different context object and return value, which makes them useful for different use cases.


Function Context object Return value Usage
let it Result of the lambda expression Execute a block of code on the result of a call chain or to work with nullable objects
run this Result of the lambda expression Often used when you want to perform multiple operations on an object and return a result
with this Result of the lambda expression Execute a block of code on an object
apply this The context object itself Typically used for initializing or configuring an object.
also it The context object itself Perform additional operations on an object without changing the object itself

let

Execute a block of code on the result of a call chain or to work with nullable objects.

The context object of the let function is referred to as it and the return value is the result of the lambda expression.

The let function is particularly useful when used with a nullable types, because it allows us to chain multiple operations on a nullable object.


Example: using let to work with nullable objects
						
							data class Message(
								val text: String?,
								var acknowledged: Boolean = false
							) {
								fun send(): Message {
									TODO("Response message")
								}
							}
						
					
						
							val message = Message(text = "Hello")

							val response = message.send()

							val text = response.text?.let {
								println("Received message: $it")
							}

							// process text
						
					

run

Often used when you want to perform multiple operations on an object and return a result.

The context object of the run function is referred to as this and the return value is the result of the lambda expression.

Example 1: Using run to initialize an object
						
							data class Message(
								val text: String?,
								var acknowledged: Boolean = false
							) {
								fun send(): Message {
									TODO("Response message")
								}
							}
						
					
						
							val message = Message(text = "Hello").run {
								if (send().acknowledged) {
									println("Message acknowledged")
								} else {
									println("Message not acknowledged")
								}
								this
							}
						
					

with

User when you want to execute a block of code on an object.
Example 1: Using run to initialize an object
						
							data class Message(
								val text: String?,
								var acknowledged: Boolean = false
							) {
								fun send(): Message {
									TODO("Response message")
								}
							}
						
					
						
							val message =  Message(text = "Hello")

							with(message) {
								if (send().acknowledged) {
									println("Message acknowledged")
								} else {
									println("Message not acknowledged")
								}
							}
						
					

apply

Typically used for initializing or configuring an object.
Example 1: Using apply to initialize an object
						
							data class Message(
								val text: String?,
								var acknowledged: Boolean = false
							) {
								fun send(): Message {
									TODO("Response message")
								}
							}
						
					
						
							   val message =  Message(text = "Hello").apply {
									acknowledged = true
								}
						
					

also

used when you want to perform additional operations on an object without changing the object itself.
Example 1: Using also to print the object
						
							data class Message(
								val text: String?,
								var acknowledged: Boolean = false
							) {
								fun send(): Message {
									TODO("Response message")
								}
							}
						
					
						
							val message = Message(text = "Hello")
								.also { println(it)  }

							val response = message.send()
								.also { println(it)  }
						
					

Exercise

You have two data classes:

						
							data class Person(
								val name: String,
								val contact: Contact
							)
						
					
						
							data class Contact(
								var email: String? = null,
								var phone: String? = null
							)
						
					

Write a code that will create an instance of a person, for example:

							
								val person = Person(
									name = "John",
									contact = Contact()
								)
							
						

Then use the scope functions to update the person's contact information. You can do all this in a main function.

Next Lesson

Next Lesson

Data Layer and Repositories