How do I build a web application using Spring MVC?

Building a Web Application Using Spring MVC

A Spring MVC web application is typically built around these pieces:

  1. DispatcherServlet — the front controller that receives HTTP requests.
  2. Spring MVC configuration — enables MVC and configures controllers, view resolution, static resources, etc.
  3. Controllers — handle web requests.
  4. Services — contain business logic.
  5. Repositories — handle persistence, often with Spring Data JPA.
  6. Views or REST responses — return HTML pages, JSON, text, etc.
  7. Deployment setup — either Spring Boot embedded server or traditional WAR deployment.

1. Choose an Application Style

There are two common ways to build Spring MVC applications.

Option A: Spring Boot MVC Application

This is the most common modern approach.

You create an executable application with an embedded server such as Tomcat.

Option B: Traditional Spring MVC WAR Application

You deploy a WAR file to an external servlet container such as Tomcat.

Both use Spring MVC, but Spring Boot reduces configuration significantly.


Option A: Spring Boot + Spring MVC

2. Add Dependencies

If using Maven, a basic Spring MVC web application can start with:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Optional: for validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!-- Optional: for JPA/database access -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
</dependencies>

spring-boot-starter-web includes Spring MVC and an embedded servlet container.


3. Create the Main Application Class

package com.example.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WebApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }
}

@SpringBootApplication enables component scanning, auto-configuration, and Spring configuration support.

Recommended package structure:

com.example.app
├── WebApplication.java
├── controller
│   └── HomeController.java
├── service
│   └── GreetingService.java
├── repository
│   └── UserRepository.java
└── model
    └── User.java

Keep the main class in the root package so Spring can scan subpackages.


4. Create a REST Controller

For JSON/text responses, use @RestController.

package com.example.app.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloRestController {

    @GetMapping("/api/hello")
    public String hello() {
        return "Hello from Spring MVC";
    }
}

Run the application and visit:

http://localhost:8080/api/hello

5. Create an MVC Controller That Returns a View

If you want server-rendered HTML pages, use @Controller.

package com.example.app.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    @GetMapping("/")
    public String home(Model model) {
        model.addAttribute("message", "Welcome to Spring MVC");
        return "home";
    }
}

With Thymeleaf, add:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Then create:

src/main/resources/templates/home.html
<!DOCTYPE html>
<html>
<head>
    <title>Spring MVC</title>
</head>
<body>
    <h1 th:text="${message}">Default message</h1>
</body>
</html>

Spring Boot automatically configures Thymeleaf templates from src/main/resources/templates.


6. Add a Service Layer

Controllers should usually delegate business logic to services.

package com.example.app.service;

import org.springframework.stereotype.Service;

@Service
public class GreetingService {

    public String getGreeting() {
        return "Hello from the service layer";
    }
}

Inject the service into a controller using constructor injection:

package com.example.app.controller;

import com.example.app.service.GreetingService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

    private final GreetingService greetingService;

    public GreetingController(GreetingService greetingService) {
        this.greetingService = greetingService;
    }

    @GetMapping("/api/greeting")
    public String greeting() {
        return greetingService.getGreeting();
    }
}

7. Handle Form Data

For a traditional web form:

package com.example.app.controller;

import com.example.app.form.ContactForm;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class ContactController {

    @GetMapping("/contact")
    public String showForm(Model model) {
        model.addAttribute("contactForm", new ContactForm());
        return "contact";
    }

    @PostMapping("/contact")
    public String submitForm(ContactForm contactForm, Model model) {
        model.addAttribute("message", "Thanks, " + contactForm.getName());
        return "contact-success";
    }
}

Form object:

package com.example.app.form;

public class ContactForm {

    private String name;
    private String email;
    private String message;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

With Lombok, this can be simplified:

package com.example.app.form;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ContactForm {

    private String name;
    private String email;
    private String message;
}

8. Add Validation

Use Jakarta Bean Validation annotations:

package com.example.app.form;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ContactForm {

    @NotBlank
    private String name;

    @Email
    @NotBlank
    private String email;

    @NotBlank
    private String message;
}

Controller:

package com.example.app.controller;

import com.example.app.form.ContactForm;
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class ContactController {

    @PostMapping("/contact")
    public String submitForm(
            @Valid ContactForm contactForm,
            BindingResult bindingResult
    ) {
        if (bindingResult.hasErrors()) {
            return "contact";
        }

        return "contact-success";
    }
}

In Spring MVC, BindingResult must immediately follow the validated argument.


9. Add Persistence with Spring Data JPA

Entity:

package com.example.app.user;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;
}

Repository:

package com.example.app.user;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

Service:

package com.example.app.user;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional(readOnly = true)
    public List<User> findAll() {
        return userRepository.findAll();
    }

    @Transactional
    public User save(User user) {
        return userRepository.save(user);
    }
}

Controller:

package com.example.app.user;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserRestController {

    private final UserService userService;

    public UserRestController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/api/users")
    public List<User> users() {
        return userService.findAll();
    }
}

Option B: Traditional Spring MVC Without Spring Boot

If you are building a classic Spring MVC application deployed as a WAR, you usually configure the application with Java configuration classes.

10. Add MVC Configuration

package com.example.app.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.example.app")
public class WebConfig implements WebMvcConfigurer {
}

@EnableWebMvc enables Spring MVC features such as request mapping, message conversion, validation support, and more.


11. Configure the DispatcherServlet

For a Servlet 3+ container, you can initialize Spring MVC without web.xml:

package com.example.app.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}

Typical separation:

RootConfig     -> services, repositories, data sources, transactions
WebConfig      -> controllers, view resolvers, Spring MVC configuration
DispatcherServlet -> receives web requests

12. Add a Root Configuration

package com.example.app.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = {
        "com.example.app.service",
        "com.example.app.repository"
})
public class RootConfig {
}

13. Configure Views

For JSP views in a traditional Spring MVC app:

package com.example.app.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
public class ViewConfig {

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
}

Or add it directly to WebConfig:

package com.example.app.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.example.app.controller")
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
}

Controller:

package com.example.app.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class PageController {

    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute("message", "Hello Spring MVC");
        return "index";
    }
}

JSP file:

src/main/webapp/WEB-INF/views/index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
    <title>Spring MVC</title>
</head>
<body>
    <h1>${message}</h1>
</body>
</html>

14. Recommended Layering

A clean Spring MVC application usually follows this flow:

HTTP Request
    ↓
DispatcherServlet
    ↓
Controller
    ↓
Service
    ↓
Repository
    ↓
Database

Example responsibilities:

Layer Annotation Responsibility
Controller @Controller, @RestController Handle HTTP requests/responses
Service @Service Business logic and transactions
Repository @Repository or Spring Data interface Data access
Entity/Model @Entity, DTOs, form objects Data structure

15. Basic REST Endpoint Example

package com.example.app.employee;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class EmployeeController {

    @GetMapping("/employees")
    public List<String> employees() {
        return List.of("Alice", "Bob", "Charlie");
    }
}

Calling:

GET http://localhost:8080/employees

returns JSON:

["Alice", "Bob", "Charlie"]

16. Common Spring MVC Annotations

Annotation Purpose
@Controller MVC controller that usually returns a view name
@RestController REST controller returning response bodies
@RequestMapping General request mapping
@GetMapping Handles HTTP GET
@PostMapping Handles HTTP POST
@PutMapping Handles HTTP PUT
@DeleteMapping Handles HTTP DELETE
@PathVariable Reads values from URI path
@RequestParam Reads query/form parameters
@RequestBody Reads JSON/XML request body
@ResponseBody Writes method return value directly to response
@ModelAttribute Binds form/model data
@Valid Triggers Jakarta Bean Validation

17. Example REST Controller with Request Body

DTO:

package com.example.app.employee;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record CreateEmployeeRequest(
        @NotBlank String name,
        @Email @NotBlank String email
) {
}

Controller:

package com.example.app.employee;

import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class EmployeeController {

    @PostMapping("/employees")
    @ResponseStatus(HttpStatus.CREATED)
    public String createEmployee(@Valid @RequestBody CreateEmployeeRequest request) {
        return "Created employee: " + request.name();
    }
}

Example request:

POST /employees HTTP/1.1
Content-Type: application/json

{
  "name": "Alice",
  "email": "[email protected]"
}

18. Handle Errors Globally

Use @ControllerAdvice for centralized exception handling.

package com.example.app.web;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleIllegalArgument(IllegalArgumentException exception) {
        return new ErrorResponse(exception.getMessage());
    }

    public record ErrorResponse(String message) {
    }
}

19. Test a Controller

With Spring Boot, you can test MVC endpoints using MockMvc:

package com.example.app.employee;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(EmployeeController.class)
class EmployeeControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void employeesReturnsList() throws Exception {
        mockMvc.perform(get("/employees"))
                .andExpect(status().isOk())
                .andExpect(content().json("[\"Alice\",\"Bob\",\"Charlie\"]"));
    }
}

20. Practical Checklist

To build a Spring MVC web application:

  1. Add Spring MVC dependencies.
  2. Create an application entry point.
  3. Enable component scanning.
  4. Create controllers with @Controller or @RestController.
  5. Add services with @Service.
  6. Add repositories with Spring Data JPA if needed.
  7. Use constructor injection.
  8. Add validation with Jakarta Bean Validation.
  9. Configure views if returning HTML.
  10. Configure persistence if using a database.
  11. Add global exception handling.
  12. Write tests for controllers and services.
  13. Run the application and test endpoints in a browser, curl, or Postman.

For most new applications, use Spring Boot with spring-boot-starter-web. For traditional servlet-container deployment, use Java config with @EnableWebMvc and a DispatcherServlet initializer.

How do I understand the coroutine lifecycle in Kotlin?

Kotlin coroutine lifecycle: the mental model

A Kotlin coroutine is a lightweight concurrent task that runs inside a CoroutineScope and is controlled by a Job.

The lifecycle is mostly about the state of that Job.

At a high level:

New → Active → Completing → Completed
              ↘ Cancelling → Cancelled

Most of the time, you mainly think about:

Active → Completed
Active → Cancelled

1. Coroutine starts in a scope

You usually launch a coroutine with launch or async:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        println("Coroutine started")
        delay(1000)
        println("Coroutine finished")
    }

    println("Job is active: ${job.isActive}")
    job.join()
    println("Job is completed: ${job.isCompleted}")
}

Here:

  • runBlocking creates a coroutine scope.
  • launch starts a child coroutine.
  • job represents that coroutine’s lifecycle.
  • join() waits until the coroutine finishes.

2. The main lifecycle states

New

A coroutine can be created but not started yet if you use CoroutineStart.LAZY.

val job = launch(start = CoroutineStart.LAZY) {
    println("Started later")
}

At this point, the coroutine exists but has not begun running.

You start it with:

job.start()

or:

job.join()

Active

Once started, the coroutine becomes active.

val job = launch {
    delay(1000)
}

While active, it can:

  • run code
  • suspend
  • resume later
  • launch child coroutines
  • be cancelled

Suspension does not mean the coroutine is inactive. A coroutine waiting in delay, for example, is still active.


Completing

When the coroutine body finishes, the coroutine enters a completing phase.

This matters especially if it has child coroutines:

val job = launch {
    launch {
        delay(1000)
        println("Child done")
    }

    println("Parent body done")
}

The parent coroutine’s body may finish quickly, but the parent Job is not fully completed until its children complete.

This is part of Kotlin’s structured concurrency model.


Completed

A coroutine is completed when:

  • its body finishes normally
  • all of its child coroutines are completed

Example:

val job = launch {
    delay(500)
    println("Done")
}

job.join()
println(job.isCompleted)

Cancelling

When cancellation is requested, the coroutine enters a cancelling state.

val job = launch {
    repeat(10) {
        delay(500)
        println("Working $it")
    }
}

delay(1200)
job.cancel()

Cancellation is cooperative. The coroutine needs to reach a cancellable suspension point or check cancellation manually.

Common cancellable suspension points include:

  • delay
  • withContext
  • yield
  • many kotlinx.coroutines suspending functions

Cancelled

Once cancellation cleanup finishes, the coroutine becomes cancelled.

val job = launch {
    try {
        delay(5000)
    } finally {
        println("Cleanup")
    }
}

delay(100)
job.cancelAndJoin()
println(job.isCancelled)

cancelAndJoin() cancels the coroutine and waits for it to fully finish.


3. Cancellation is cooperative

This coroutine cancels easily:

val job = launch {
    while (isActive) {
        println("Working")
        delay(100)
    }
}

delay(500)
job.cancelAndJoin()

This one may not cancel quickly:

val job = launch {
    while (true) {
        // CPU-heavy work, no suspension, no cancellation check
    }
}

For CPU-bound work, check cancellation manually:

val job = launch {
    while (isActive) {
        // Do a chunk of work
    }
}

Or call:

ensureActive()

Example:

val job = launch {
    repeat(1_000_000) {
        ensureActive()
        // Do work
    }
}

4. Parent-child relationship

Coroutines launched inside another coroutine become its children:

fun main() = runBlocking {
    val parent = launch {
        launch {
            delay(1000)
            println("Child finished")
        }

        println("Parent body finished")
    }

    parent.join()
    println("Parent fully completed")
}

Output is conceptually:

Parent body finished
Child finished
Parent fully completed

The parent waits for the child before becoming completed.


5. Cancelling a parent cancels its children

val parent = launch {
    launch {
        repeat(10) {
            delay(300)
            println("Child working")
        }
    }
}

delay(700)
parent.cancelAndJoin()

When the parent is cancelled, its child coroutine is cancelled too.

This is one of the most important lifecycle rules.


6. Child failure usually cancels the parent

By default, if a child coroutine fails with an exception, the parent is cancelled:

val parent = launch {
    launch {
        throw RuntimeException("Child failed")
    }

    launch {
        delay(1000)
        println("This may be cancelled")
    }
}

This behavior keeps concurrent work grouped together: if one part fails, the whole operation usually fails.

If you want children to fail independently, use supervisorScope or SupervisorJob.

supervisorScope {
    launch {
        throw RuntimeException("Child failed")
    }

    launch {
        delay(1000)
        println("Still runs")
    }
}

7. launch vs async

Both create coroutines, but they expose results differently.

launch

Use launch for fire-and-forget work that does not return a value.

val job: Job = launch {
    println("Doing work")
}

It returns a Job.


async

Use async for work that returns a value.

val deferred: Deferred<Int> = async {
    delay(500)
    42
}

val result = deferred.await()
println(result)

It returns a Deferred<T>, which is also a Job.

await() waits for completion and returns the result, or throws the coroutine’s exception.


8. Cleanup with finally

A coroutine can clean up when it completes or is cancelled:

val job = launch {
    try {
        repeat(10) {
            delay(300)
            println("Working $it")
        }
    } finally {
        println("Cleaning up")
    }
}

delay(800)
job.cancelAndJoin()

If you need to call a suspending function during cleanup, use NonCancellable:

val job = launch {
    try {
        delay(5000)
    } finally {
        withContext(NonCancellable) {
            delay(300)
            println("Cleanup finished")
        }
    }
}

Use this sparingly, because it delays cancellation.


9. Common lifecycle operations

Operation Meaning
job.start() Starts a lazy coroutine
job.join() Waits for coroutine completion
job.cancel() Requests cancellation
job.cancelAndJoin() Cancels and waits until finished
job.isActive true while active
job.isCompleted true after completion or cancellation
job.isCancelled true if completed due to cancellation

10. Practical lifecycle example

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            println("Started")

            repeat(5) { index ->
                delay(500)
                println("Working $index")
            }

            println("Finished normally")
        } catch (e: CancellationException) {
            println("Cancelled")
            throw e
        } finally {
            println("Cleanup")
        }
    }

    delay(1200)

    println("Requesting cancellation")
    job.cancelAndJoin()

    println("Job completed: ${job.isCompleted}")
    println("Job cancelled: ${job.isCancelled}")
}

Possible output:

Started
Working 0
Working 1
Requesting cancellation
Cancelled
Cleanup
Job completed: true
Job cancelled: true

Simple way to remember it

Think of a coroutine like this:

Created in a scope
→ runs as a Job
→ may suspend and resume many times
→ finishes successfully, fails, or gets cancelled
→ parent and children affect each other

The key concepts are:

  1. Scope owns the coroutine
  2. Job represents its lifecycle
  3. Suspension is not blocking
  4. Cancellation is cooperative
  5. Parents wait for children
  6. Parent cancellation cancels children
  7. Child failure usually cancels the parent

How do I cancel a coroutine in Kotlin safely?

In Kotlin, you cancel a coroutine safely by calling cancel() on its Job or scope, and making sure the coroutine code is cooperative: it should suspend regularly or check for cancellation.

Basic example

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1_000) { i ->
                println("Working $i")
                delay(500) // cancellable suspension point
            }
        } finally {
            println("Cleaning up")
        }
    }

    delay(1_500)
    job.cancel()
    job.join()

    println("Cancelled safely")
}

Or more commonly:

job.cancelAndJoin()
job.cancelAndJoin()

This cancels the coroutine and waits until it finishes cleanup.

Use cancellable suspending functions

Most kotlinx.coroutines suspending functions are cancellable, for example:

delay(...)
withContext(...)
receive(...)
send(...)

So this is usually safe:

val job = scope.launch {
    while (true) {
        delay(1000)
        doWork()
    }
}

job.cancel()

For CPU-heavy loops, check cancellation manually

If your coroutine does not suspend often, cancellation will not be noticed immediately.

Use isActive:

val job = scope.launch {
    while (isActive) {
        doCpuWorkChunk()
    }
}

Or call ensureActive():

val job = scope.launch {
    while (true) {
        ensureActive()
        doCpuWorkChunk()
    }
}

Cleanup with finally

Use try/finally for cleanup:

val job = scope.launch {
    try {
        doWork()
    } finally {
        closeResources()
    }
}

If cleanup needs to call suspending functions, use NonCancellable:

val job = scope.launch {
    try {
        doWork()
    } finally {
        withContext(NonCancellable) {
            saveState()
            closeRemoteConnection()
        }
    }
}

Do not swallow CancellationException

Cancellation is represented by CancellationException. Avoid catching it accidentally and ignoring it.

Bad:

try {
    doWork()
} catch (e: Exception) {
    // This also catches CancellationException
    logError(e)
}

Better:

try {
    doWork()
} catch (e: CancellationException) {
    throw e
} catch (e: Exception) {
    logError(e)
}

Cancel a scope

If you created a scope, cancel it when the owner is destroyed:

class Repository {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    fun start() {
        scope.launch {
            doWork()
        }
    }

    fun close() {
        scope.cancel()
    }
}

Summary

Use:

job.cancelAndJoin()

Make your coroutine cooperative by using:

delay(...)
isActive
ensureActive()

Clean up with:

try {
    // work
} finally {
    // cleanup
}

And avoid swallowing CancellationException.

How do I run coroutines in parallel using async and await in Kotlin?

In Kotlin, use async to start concurrent work inside a coroutine scope, then call await() to get each result.

Basic example:

import kotlinx.coroutines.*

suspend fun fetchUser(): String {
    delay(1000)
    return "User"
}

suspend fun fetchPosts(): List<String> {
    delay(1000)
    return listOf("Post 1", "Post 2")
}

fun main() = runBlocking {
    val userDeferred = async {
        fetchUser()
    }

    val postsDeferred = async {
        fetchPosts()
    }

    val user = userDeferred.await()
    val posts = postsDeferred.await()

    println(user)
    println(posts)
}

Here, fetchUser() and fetchPosts() run in parallel. Since both delay for 1 second, the total time is about 1 second instead of 2.

You can also await multiple results with awaitAll:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferredResults = listOf(
        async {
            delay(1000)
            "Result 1"
        },
        async {
            delay(1000)
            "Result 2"
        },
        async {
            delay(1000)
            "Result 3"
        }
    )

    val results = deferredResults.awaitAll()

    println(results)
}

Output:

[Result 1, Result 2, Result 3]

A common pattern is:

coroutineScope {
    val a = async { doWorkA() }
    val b = async { doWorkB() }

    val resultA = a.await()
    val resultB = b.await()

    combine(resultA, resultB)
}

For example:

import kotlinx.coroutines.*

suspend fun loadName(): String {
    delay(500)
    return "Alice"
}

suspend fun loadAge(): Int {
    delay(500)
    return 30
}

suspend fun loadProfile(): String = coroutineScope {
    val name = async { loadName() }
    val age = async { loadAge() }

    "${name.await()} is ${age.await()} years old"
}

fun main() = runBlocking {
    println(loadProfile())
}

Important notes:

  • async returns a Deferred<T>.
  • await() suspends until the result is ready.
  • async should usually be called inside coroutineScope, supervisorScope, runBlocking, or another coroutine.
  • If one async child fails inside coroutineScope, the whole scope is cancelled.
  • Use async for concurrent computations that return values.
  • Use launch for concurrent work that does not return a value.

If you are doing blocking I/O, use an appropriate dispatcher:

val data = async(Dispatchers.IO) {
    blockingNetworkCall()
}

For CPU-heavy work:

val result = async(Dispatchers.Default) {
    heavyCalculation()
}

How do I write a suspend function in Kotlin?

In Kotlin, a suspend function is declared with the suspend modifier.

suspend fun fetchUser(): User {
    // Can call other suspend functions here
    return api.getUser()
}

A suspend function can pause without blocking the thread and later resume. It is commonly used with coroutines for asynchronous work.

Example:

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking

suspend fun greetAfterDelay() {
    delay(1000)
    println("Hello after 1 second")
}

fun main() = runBlocking {
    greetAfterDelay()
}

Key points:

  • Use suspend fun to define one.
  • A suspend function can call other suspend functions.
  • It must be called from:
    • another suspend function, or
    • a coroutine scope such as runBlocking, launch, or async.

Example with return value:

suspend fun loadMessage(): String {
    delay(500)
    return "Loaded message"
}

fun main() = runBlocking {
    val message = loadMessage()
    println(message)
}

So the basic syntax is:

suspend fun functionName() {
    // suspending work
}