Back to articles list
- 20 minutes read

How to Develop a Simple Tic Tac Toe Game with Spring Boot and AngularJS

Everyone likes to play games, especially the simple ones. Tic tac toe is about as simple as you can get, and despite its age it is still widely played. In the past, the only way to play tic tac toe was on paper; now there are plenty of computerized options. In this article (inspired by our recent post, A Database Model for Simple Board Games), we’ll present a tutorial on building a tic tac toe web application with Spring Boot and AngularJS. The final code is hosted on GitHub.

Table of Contents


The Technology Stack

The Database Model

The Technology




We’ll begin with a gentle introduction to the development of our app, starting with a basic overview of the game’s functionality. The final application will consist of three screens, one of which is automatically generated by the Spring Security framework.

  1. The custom login page, automatically rendered by Spring Security:

    Loin page

  2. The screen where players can create a new game, join an existing game, or load a previous game:

    Player panel

  3. The game screen:

    Game screen

The Technology Stack

These are the technologies we will use to build our app:

  • Spring Boot – A Spring-based application, ready to “run” with no additional configuration
  • Spring Data JPA – A Spring extension that allows the creation of repositories based on Spring and JPA
  • AngularJS – A JavaScript MVC framework
  • Bootstrap – A CSS framework


This game will use several dependencies. They are added to the classpath with Gradle.

To see them all, please check the dependencies closure in build.gradle; here’s a partial list:

dependencies {

The Database Model

Let’s first inspect the database behind the game. Below, you will find the model that the application will use:

For the application’s purposes, we need three tables. The game table stores game details, the move table contains a list of all player moves for each particular game, and the player table stores all the player-specific data.

The Game Table

Game table

The game table contains these columns:

  • first_player_id: references the Player table. Its value relates to the player that created the game.
  • second_player_id: references the second player in the game. This value can be nullable, because the second player may be the application (“COMPUTER”).
  • created: contains the date and time when the game was created.
  • game_status: contains possible game statuses: WAITS_FOR_PLAYER, IN_PROGRESS, FIRST_PLAYER_WON, SECOND_PLAYER_WON, TIE. Its value is checked by the database in a CHECK constraint.
  • game_type: contains two possible game types: COMPUTER or COMPETITION. This value is also checked by the database in a CHECK constraint.
  • first_player_piece_code: stores the values ‘X’ or ‘O’. This value too is checked by a CHECK constraint.

Object-Relational Mapping with Hibernate

We’re using Hibernate for object-relational mapping. Hibernate follows the “object-first” approach. It means that the appropriate database structures are generated based on the Java code.

Here is the Game class, which is mapped to the game table. Notice that no constructors or getters and setters are written explicitly. Rather, they are generated. We’re using the Lombok library for this.

As you see, the Game class is annotated by @Getter and @Setter. They are responsible for generating getters and setters for each field in the class. The @NoArgsConstructor and @AllArgsConstructor annotators are responsible for generating the default and all-args constructors respectively.

A Hibernate Crib Sheet:
@EntityMarks a class as an entity bean
@IdMarks a field as a primary key
@GeneratedValueDefines a primary key generation strategy
@ColumnSpecifies the details of the column to which the field will be mapped
@ManyToOneMaps a many-to-one relationship
@JoinColumnIndicates the entity is the owner of the relationship (The corresponding table has a column with a foreign key to the referenced table.)
@EnumeratedConverts database data to and from Java enum types
@CheckDefines the optional check constraint based on the SQL statement

Here’s the code:

@Check(constraints = "first_player_piece_code = 'O' or first_player_piece_code = 'X' " +
       "and game_type = 'COMPUTER' or game_type = 'COMPETITION' " +
       "and game_status = 'IN_PROGRESS' or game_status = 'FIRST_PLAYER_WON' or game_status = 'SECOND_PLAYER_WON'" +
       "or game_status = 'TIE' or game_status = 'WAITS_FOR_PLAYER' ")

public class Game {

   @GeneratedValue(strategy = GenerationType.AUTO)
   @Column(name = "id", nullable = false)
   private Long id;

   @JoinColumn(name = "second_player_id", nullable = true)
   private Player secondPlayer;

   @JoinColumn(name = "first_player_id", nullable = false)
   private Player firstPlayer;

   private Piece firstPlayerPieceCode;

   private GameType gameType;

   private GameStatus gameStatus;

   @Column(name = "created", nullable = false)
   private Date created;

The Move Table

The move table will store all information related to players’ moves. Looking at this table, we can see who made what move, in which game, at what time, and in which cell.

Move table

The Java entity that will generate this table is very similar to the one used for the Game entity. We’ve omitted it here for the sake of clarity. To check it, please visit the app repository on GitHub.

The Player Table

Finally, take a look at the player table. It stores player-specific data: each player’s user_name, password_hash, and email.

Player table

Once again, we’ve omitted the Java entity here for clarity’s sake; it’s quite like the Game one, too. To check it, please visit the app repository on GitHub.

The Technology

Spring Boot

Because this application consists of many elements, I want to take a moment to introduce Spring Boot’s content. Let’s start with the packages. When you take a look at the application’s code on GitHub, you will see that there are many separated layers. This is also shown below:

Layers Spring Boot application

Let’s briefly list what’s going on in here. Most of it is self-explanatory:

  • The config package contains the HTTP Security configuration.
  • The controller package contains classes that handle HTTP requests.
  • The domain package contains the entity classes.
  • The DTO package contains the classes used to map request parameters.
  • The enums package contains the enums used in entity classes and that are mapped to appropriate columns in the game table. In this case, the enums are GameStatus (possible game statuses), GameType (possible game types) and Piece (pieces available in the game).
  • The repository package contains the repository interfaces that provide CRUD functionality to the entity classes.
  • The security package contains the classes responsible for user authentication.
  • The service package identifies the application layer that encapsulates the business logic, controls transactions, etc.


The Angular application content is located in the webapp package:

Angular application

The Angular application’s main configuration is placed in the application.js file. As you can see below, the first line of this file is the ticTacToe module declaration, which specifies how the application should be bootstrapped. (The dependency modules are in the square brackets). After that, the routing config is specified: this divides the application into logical views and binds the different views to controllers.

var ticTacToe = angular.module('ticTacToe', ['ngRoute', 'gameModule']);

ticTacToe.config(['$routeProvider', function($routeProvider) {
       when('/about', {
           templateUrl: 'templates/about.html'
       when('/game/:id', {
           templateUrl: 'templates/game-board.html',
           controller: 'gameController'
       when('/player/panel', {
           templateUrl: 'templates/player-panel.html',
           controller: 'newGameController'
           redirectTo: '/player/panel'

The above code defines three urls that are mapped by views: templates/about.html, templates/game-board.html and templates/player-panel.html. For example, when we open http://localhost:8080/#/player/panel in the browser, Angular automatically matches it with the route we configured and loads the templates/player-panel.html template. It then invokes newGameController, where the logic for the view is added.


When you take a look at the GitHub code, you will definitely agree that the application details create a lot of room for discussion – more than we can fit for here. That’s why we’re focusing only on particular elements. When describing the selected functionality, we will start with what we see on the screen and end with the database.

User Authentication with Spring Security

Whenever we start the game app, we will be redirected to an automatically-generated login form. I decided not to worry about user authentication and leave it to Spring Security. Consequently, the Spring application will present a login form to the player, check whether each request is carried out by an authenticated user, etc. Before we move to the Java code, have a look at what the login screen looks like:

Login page

We’ve already placed Spring Security in the dependencies section in build.gradle.We’ll now examine our configuration – specifically, the code for two classes: ContextUser and UserDetailsServiceImpl.

Let’s start with the ContextUser class. Here we construct the user object by extending the User class from Spring Security with the details required by DaoAuthenticationProvider(). In this case, the credentials will be the player’s username and password. If the user provides a correct username and password combination, they will be granted “create” authority. Other parameters in the constructor are set to true; we are not concerned if the account has expired or if the app is locked because it’s being used as a demo.

public class ContextUser extends {

   private final Player player;

   public ContextUser(Player player) {
               ImmutableSet.of(new SimpleGrantedAuthority("create")));

       this.player = player;

   public Player getPlayer() {
       return  player;

Let’s go further and customize the security settings. This time, take a look at the UserDetailsServiceImpl class.

public class UserDetailsServiceImpl implements UserDetailsService {
   private final PlayerRepository playerRepository;

   public UserDetailsServiceImpl(PlayerRepository playerRepository) {
       this.playerRepository = playerRepository;

   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

       if(isEmpty(username)) {
           throw new UsernameNotFoundException("Username cannot be empty");

       Player player = playerRepository.findOneByUserName(username);
       if (player == null) {
           throw new UsernameNotFoundException("Player " + username + " doesn't exists");
       return new ContextUser(player);

Next, let’s consider the SecurityConfig class, which you can find in the config package. This class ensures that only authenticated users can access application pages. The SecurityConfig class is annotated with @EnableWebSecurity to enable Spring Security’s web security support. The class also extends WebSecurityConfigurerAdapter and overrides a couple of methods to set some specifics for web security configuration.

Now have a look at configure(HttpSecurity http). We will override this method to configure Spring’s HttpSecurity. This class tells Spring:

  • that any requested URL will require an authenticated user.
  • the specifics of the form-support-based authentication, which will generate the default login page.

The interface UserDetailsService loads user-specific data. It is employed throughout the framework as a user DAO.

public class SecurityConfig extends WebSecurityConfigurerAdapter{

   private PlayerRepository playerRepository;

   public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
               .userDetailsService(new UserDetailsServiceImpl(playerRepository))
               .passwordEncoder(new BCryptPasswordEncoder());

   protected void configure(HttpSecurity http) throws Exception {

The Player Screen

We now are done with user authentication and can move on to the actual game. After logging in, the player is redirected to the /player/panel endpoint. In this page, they can:

  1. Create a new game and play against an AI opponent (the ‘COMPUTER’).
  2. Create a new game and play against a human.
  3. Join a two-player game.
  4. Resume a game.

Player panel actions

Ad1 and Ad2 : Creating a New Game

From the drop-down menu, the user can choose one of two Game types. The ‘COMPUTER’ value will create a game with the ‘COMPUTER’ type, in which the application is the second player. The second type is ‘COMPETITION’, in which two human opponents play against each other.

The next field we have is Play as. This lets the player choose which piece they will use ( X or O). After clicking the New Game button, the selected values are sent as a JSON object in the POST request to the /game/create endpoint.

Creating new game actions

The appropriate sequence of actions are visualized in the diagram below.

POST /game/create

Let’s pick up the action with moving to the Angular application. In gameModule.js, we have the controller (newGameController) where the logic for creating a new game is located. In particular, please look at the createNewGame() function. It is created in the scope (an object containing model data). A scope joins the controller with the views.

The above-mentioned POST request is made by Angular. Below is the function that sends the POST request:

       scope.createNewGame = function () {

           var data = scope.newGameData;
           var params = JSON.stringify(data);

 "/game/create", params, {
               headers: {
                   'Content-Type': 'application/json; charset=UTF-8'
           }).success(function (data, status, headers, config) {
               rootScope.gameId =;
               location.path('/game/' + rootScope.gameId);
           }).error(function (data, status, headers, config) {

Now, let’s move to the Spring Boot GameController class that will handle this request. In this case, createNewGame() is responsible. Here’s the code:

@RequestMapping(value = "/create")
public Game createNewGame(@RequestBody GameDTO gameDTO) {

   Game game = gameService.createNewGame(playerService.getLoggedUser(), gameDTO);
   httpSession.setAttribute("gameId", game.getId());

   return game;

The value placed in the @RequestMapping annotation maps the web request to the appropriate function. The request body will be mapped to GameDTO by the @RequestBody annotation. GameDTO can appropriately request two fields: GameType and Piece.

Class GameDTO

For creating the object, we use the createNewGame() function from the service package. This takes two arguments: the logged user object, and the gameDTO object. After creating the game, a game ID is saved in the session.

public Game createNewGame(Player player, GameDTO gameDTO) {
   Game game = new Game();
   game.setGameStatus(gameDTO.getGameType() == GameType.COMPUTER ? GameStatus.IN_PROGRESS :

   game.setCreated(new Date());;

   return game;

In the response, the Angular application receives the new game’s object and redirects the user to a new game page.

Ad3. Join a Two-Player Game

At the right side of the screen, we can see games created by other users. To get the list of those games,

Angular sends a GET request to the /game/list endpoint. The user can join a game by clicking the Join button

Join existing game

Once again, let’s take a look at Angular’s role. The Angular application sends the HTTP GET request to the /game/list endpoint. The appropriate code is below.

http.get('/game/list').success(function (data) {
   scope.gamesToJoin = data;
}).error(function (data, status, headers, config) {

The Spring Boot function that handles the /game/list endpoint returns a list of games. This is what the code looks like:

@RequestMapping(value = "/list", produces = MediaType.APPLICATION_JSON_VALUE)
public List getGamesToJoin() {
   return gameService.getGamesToJoin(playerService.getLoggedUser());

Let’s now take a look at the GameService class:

public List getGamesToJoin(Player player) {
   return gameRepository.findByGameTypeAndGameStatus(GameType.COMPETITION,
           GameStatus.WAITS_FOR_PLAYER).stream().filter(game -> game.getFirstPlayer() != player).collect(Collectors.toList());

The function findByGameTypeAndGameStatus() returns a list of games that are of the COMPETITION type and have the WAITS_FOR_PLAYER status. Unfortunately, this list also contains games created by the current user. So each of the games is filtered in the stream and the appropriate objects collected in a separate list. If the object of firstPlayer in the processed game object differs from the object of the currently logged player, that game is added to the list.

Ad4. Resume a Game

My games

As with the previous action, Angular again sends a request for the player’s games:

http.get('/game/player/list').success(function (data) {
   scope.playerGames = data;
}).error(function (data, status, headers, config) {

The Java function that handles this request is as follows:

@RequestMapping(value = "/player/list", produces = MediaType.APPLICATION_JSON_VALUE)
public List getPlayerGames() {
   return gameService.getPlayerGames(playerService.getLoggedUser());

The getPlayerGames() function takes only one argument, which is the object of the currently logged user. This is retrieved from the session. Let’s move to game services and look at this function:

public List getPlayerGames(Player player) {
   return gameRepository.findByGameStatus(
           GameStatus.IN_PROGRESS).stream().filter(game -> game.getFirstPlayer() == player).collect(Collectors.toList());

The function findByGameStatus() returns games with the status IN_PROGRESS . The result contains all users’ games, filtered by the player’s object. If the firstPlayer object of the game is the same as the current player, the game is added to the list. The list of filtered games is returned to Angular and displayed to the user.

Playing the Game

After a new game is created, the user sees a fresh page. On the left side, there is an empty board; the right side is dedicated to moves performed during the game.

Empty board

The board is generated from a defined array of rows. Each row and cell has a CSS class set.

Below is the HTML that corresponds to the board you see above. The individual cells are generated from the rows array:

This is the content of the array that stores the rows’ initial values. At the beginning, each letter has an empty value. After marking the move, the value will change to ‘X’ or ‘O’ and a different CSS will be used for that particular set.

scope.rows = [
       {'id': '11', 'letter': '', 'class': 'box'},
       {'id': '12', 'letter': '', 'class': 'box'},
       {'id': '13', 'letter': '', 'class': 'box'}
       {'id': '21', 'letter': '', 'class': 'box'},
       {'id': '22', 'letter': '', 'class': 'box'},
       {'id': '23', 'letter': '', 'class': 'box'}
       {'id': '31', 'letter': '', 'class': 'box'},
       {'id': '32', 'letter': '', 'class': 'box'},
       {'id': '33', 'letter': '', 'class': 'box'}

angular.forEach(scope.rows, function (row) {
   row[0].letter = row[1].letter = row[2].letter = '';
   row[0].class = row[1].class = row[2].class = 'box';

We now have generated the board. Next, we want to mark a move. Let’s take a look at the sequence of actions involved in this step.

Marking move

Once the user clicks on a particular cell, two important actions happen in the Angular application.

  1. Check if the board cell is available. After each move, Angular sends a GET request to the /move/list endpoint. This retrieves the list of moves for that game. Angular then saves the JSON object in the movesInGame variable. These values appear on the right side of the game screen and also are used for checking cell availability and game status. To check if a move is valid, we’re simply looking for matching row and column values in movesInGame:

    function checkIfBoardCellAvailable(boardRow, boardColumn) {
          for (var i=0; i < scope.movesInGame.length; i++) {
              var move = scope.movesInGame[i];
              if(move.boardColumn == boardColumn && move.boardRow == boardRow) {
                  return false;
          return true;

  2. Check if it is the player’s turn: The Angular application sends a GET request to the /move/turn endpoint (GET /move/turn), along with the JSON object that contains the column and row of the target cell. In response, Angular receives a boolean value:

    • true if it is the player’s turn,
    • false if it is not.

    The code for this is:

    function checkPlayerTurn() {
       http.get('/move/turn').success(function(data) {
           scope.playerTurn = data;
       }).error(function(data, status, headers, config) {
           scope.errorMessage = "Failed to get the player turn"

  3. Mark the move. Now we can finally create the move. The Angular application sends the JSON object with the board’s row and column in the POST request to the /move/create endpoint. After the move is created, the Move history section is updated. If the game is still IN_PROGRESS, we allow another move. If the status is different (‘FIRST_PLAYER_WON’, ‘SECOND_PLAYER_WON’, ‘TIE’), we show the user an alert that displays the game status and we end the game. Here’s the relevant code:

    scope.markPlayerMove = function (column) {
       checkPlayerTurn().success(function () {
           var boardRow = parseInt(;
           var boardColumn = parseInt(;
           var params = {'boardRow': boardRow, 'boardColumn': boardColumn}
           if (checkIfBoardCellAvailable(boardRow, boardColumn) == true) {
               // if player has a turn
               if (scope.playerTurn == true) {
         "/move/create", params, {
                       headers: {
                           'Content-Type': 'application/json; charset=UTF-8'
                   }).success(function () {
                       getMoveHistory().success(function () {
                           var gameStatus = scope.movesInGame[scope.movesInGame.length - 1].gameStatus;
                           if (gameStatus == 'IN_PROGRESS') {
                           } else {
                   }).error(function (data, status, headers, config) {
                       scope.errorMessage = "Can't send the move"

The request with the data is sent. What happens next? The Spring Boot application receives the JSON object with the cell details (board row and board column). The request body is mapped to the createMoveDTO object. The CreateMoveDTO class looks as follows:

Class CreateMoveDTO

Take a moment to consider the function presented below. In it, we invoke the createMove() function from the move service. It takes three arguments: the game object, the current player object, and the createMoveDTO object with data from the request. After saving the move, the updateGameStatus() function is invoked. It looks at the moves in Game and checks if the situation in the game has changed after the creation of a new move. If it has, the game will change its status.

@RequestMapping(value = "/create", method = RequestMethod.POST)
public Move createMove(@RequestBody CreateMoveDTO createMoveDTO) {
   Long gameId = (Long) httpSession.getAttribute("gameId");"move to insert:" + createMoveDTO.getBoardColumn() + createMoveDTO.getBoardRow());

   Move move = moveService.createMove(gameService.getGame(gameId), playerService.getLoggedUser(), createMoveDTO);
   Game game = gameService.getGame(gameId);
   gameService.updateGameStatus(gameService.getGame(gameId), moveService.checkCurrentGameStatus(game));

   return move;

Retrieving the List of Game Moves

The actual list of the moves in each game is displayed in the right side of the game screen. This section is titled “History of moves in the game” and presents the details about all moves made in a single game. The values from this section are updated after every move or when the game page refreshes. Below, you’ll find a screenshot:

Get move list

The Angular application stores the list of moves in scope.movesInGame. The board is displayed based on those values. We want to analyze the sequence of actions individually. Please take a look at the following image:

Get move list

After each move or page refresh, the Angular application sends the GET request to the /move/list endpoint.

For this, we use the function getMoveHistory(). It uses the Angular HTTP service to make a GET request. The function returns a Promise object, which will be resolved to a response object when the request succeeds or fails. Here’s how it looks in code:

function getMoveHistory() {
   scope.movesInGame = [];

 return  http.get('/move/list').success(function (data) {
       scope.movesInGame = data;
       scope.playerMoves = [];

       //paint the board with positions from the retrieved moves
       angular.forEach(scope.movesInGame, function(move) {
           scope.rows[move.boardRow - 1][move.boardColumn - 1].letter = move.playerPieceCode;
       }).error(function (data, status, headers, config) {
          scope.errorMessage = "Failed to load moves in game"

The request to the server was sent, so now we can move back to the Spring Boot application. The Spring function that handles this request is presented below. Based on the gameId stored in the session, the game object is retrieved from the database. Then, the function getMovesInGame() retrieves the game’s moves.

@RequestMapping(value = "/list", method = RequestMethod.GET)
public List getMovesInGame() {
   Long gameId = (Long) httpSession.getAttribute("gameId");

 return moveService.getMovesInGame(gameService.getGame(gameId));

The responsibility of the getMovesInGame() function is to retrieve the moves.

public List getMovesInGame(Game game) {

   List movesInGame = moveRepository.findByGame(game);
   List moves = new ArrayList<>();
   Piece currentPiece = game.getFirstPlayerPieceCode();

   for(Move move :  movesInGame) {
       MoveDTO moveDTO = new MoveDTO();
       moveDTO.setUserName(move.getPlayer() == null ? GameType.COMPUTER.toString() : move.getPlayer().getUserName() );

       currentPiece = currentPiece == Piece.X ? Piece.O : Piece.X;

   return moves; 

Second Player Moves

Second Player Moves

What happens next? Before we perform any other actions, we need to check the status of the game. If the status is still IN_PROGRESS, a move can be made. In games played against ‘COMPUTER’, the application decides the next move. In a two-player ‘COMPETITION’ game, the second player to join the game decides the next move.

The logic for second player moves is the same as used for the first player’s moves. It means that the move of the second player is sent with a POST request to the /move/create endpoint after checking if the particular board cell is available, etc.

The question is: How will we know when it is my turn to move? For that, we’ll use JavaScript’s checkTurn() function, which asynchronously sends a GET request to the Spring Boot application. In response, the Angular app receives a false or true value and lets the user know if they can move.

For the ‘COMPUTER’ type of game, the GET request is made after the human player moves. The AI “decides” on a play, and moves are once again retrieved. Depending on the outcome, the game either finishes or goes on:

function getNextMove() {
scope.nextMoveData = []
   http.get("/move/autocreate").success(function (data, status, headers, config) {
       scope.nextMoveData = data;
       getMoveHistory().success(function () {
           var gameStatus = scope.movesInGame[scope.movesInGame.length - 1].gameStatus;
           if (gameStatus != 'IN_PROGRESS') {
   }).error(function (data, status, headers, config) {
       scope.errorMessage = "Can't send the move"

How is the next computer move created? Let’s look to the MoveController class and explore the controller function that handles the GET request to the /move/autocreate endpoint.

@RequestMapping(value = "/autocreate", method = RequestMethod.GET)
public Move autoCreateMove() {
   Long gameId = (Long) httpSession.getAttribute("gameId");"AUTO move to insert:" );

   Move move = moveService.autoCreateMove(gameService.getGame(gameId));

   Game game = gameService.getGame(gameId);
   gameService.updateGameStatus(gameService.getGame(gameId),    moveService.checkCurrentGameStatus(game));

   return move;

In order for the computer “player” to move, we need to invoke the autoCreateMove() function from the MoveService class. The boardColumn and boardRow are chosen by the nextAutoMove() function from the GameLogic class.

public Move autoCreateMove(Game game) {
   Move move = new Move();
   move.setCreated(new Date());

   return move;

After the autocreate move is marked, gameStatus is checked and updated if needed.


We now have an overview of how a simple web-based game can be created. It’s all thanks to Spring Boot and AngularJS. If you’re interested in learning more about these applications, I strongly encourage you to check out their great documentations at Spring Boot Docs and Angular JS Docs.

Once again, If you want to take a look at the code for our tic tac toe game, visit the GitHub repository.

go to top