Sections of the tutorial will continuously be published at this web page.

This tutorial serves to give you the practical knowledge required to execute the group project in true AGILE fashion.

The tutorial will use a simple event registration app as a running example. The event registration app allows users to sign up and register for events using their name.

1. Requirements Engineering

Before designing and implementing the event registration app, we must precisely specify the features and characteristics we expect from it.

1.1. Functional Requirements

Functional requirements describe the functionality (i.e., features) we expect the app to have. It might be helpful to start with the high-level requirements (i.e., user requirements) and then derive more specific requirements (i.e., system requirements) from those. Here are examples of features that the event registration app should have.

Example 1. User requirements
  1. The event registation application shall allow prospective users to create an account.

  2. The event registration application shall allow users to create events.

  3. The event registration application shall allow users to view events.

  4. The event registration application shall allow users to register for events.

Notice how, even for user requirements, our requirements describe the system’s behaviour, they do not place requirements on or make assumptions about the user.

Now, for each user requirement, we can derive one or more system requirements.

Example 2. System requirements
  1. The event registation application shall allow prospective users to create an account.

    1. The event registration application shall allow prospective users to create an account by specifying a name and a password.

    2. The event registration application shall record the date of registration for all new users.

  2. The event registration application shall allow users to create events.

    1. The event registration application shall allow users to create an online event by specifying an event name, a date, a start time, an end time, and a URL.

    2. The event registration application shall allow users to create an in-person event by specifying an event name, a date, a start time, an end time, and an address.

  3. The event registration application shall allow users to view events.

    1. The event registration application shall allow users to view a list containing a summary of every event. For each event, the summary shall include the event name and date.

    2. The event registration application shall allow users to filter the list of events by name.

    3. The event registration application shall allow users to filter the list of events by a range of dates.

    4. The event registration shall allow users to view the full details of a specific event, including name, date, start and end times, and address or URL.

Note that not all system functional requirements need to be derived from user requirements. For example, we might have requirements describing processes that happen automatically and are not visible to end users.

Example 3. System requirements not derived from a user requirement
  1. Every Sunday at 23:30 EST, the event registration application shall automatically delete all events whose end date has passed.

  2. It shall be possible to update the event registration application at any time with no more than 15 minutes of downtime.

1.2. Non-Functional Requirements

Non-functional requirements describe characteristics that we expect the application to have. For example, we might have expectations about the system’s speed, the amount of memory or storage it uses, how user-friendly it is, etc. Here are some examples of non-functional requirements for the event registration app.

Example 4. Speed
  1. When a user creates a valid event, the event registration application shall save the event and provide confirmation in no more than 2 seconds, 95% of the time.

Example 5. Memory consumption
  1. The event registration application shall not use more than 250 MB of memory on an end user’s computer at any time.

Example 6. Usability
  1. The event registation application shall be sufficiently user-friendly that a new user with basic computer literacy shall be able to create their account in no more than 15 minutes.

1.3. Use Case Diagram

We also want to describe the use cases for the event registration application using a use case diagram.

UseCaseDiagram

The diagram above shows the following.

  • Any user can search for and view events.

  • To register for an event, a user must have an account.

  • Premium users have all the same privileges as a regular account holder. In addition, they can create events.

  • Only guests can sign up and log in (users who are already logged in don’t need these features).

1.4. Use Case Specifications

A detailed use case specification describes a use case in greater detail. It should have a main success scenario, as well as alternate scenarios in case something unusal happens. Steps are numbered. Alternatives to step 1 are labelled 1a, 1b, and so on. Then the steps in the alternative scenario 1a are labelled 1a.1, 1a.2, and so on. An alternative scenario may end in success, it may end in failure, or the use case may continue at another step.

  • ID: UC1

  • Title: Create in-person event

  • Description: An account holder schedules an in-person event.

  • Actor: An account holder

  • Main scenario:

    • 1) The user indicates that they want to create an in-person event.

    • 2) The system displays the in-person event creation form.

    • 3) The user enters an event name, date, start time, end time, and address.

    • 4) The system confirms that the event was created successfully.

  • Alternative scenarios:

    • 4a) The user entered an empty event name.

      • 4a.1) The system informs the user that the event name is required.

      • 4a.2) The use case continues at step 3.

    • 4b) The user selected a start datetime or an end datetime in the past.

      • 4b.1) The system informs the user that events can only be scheduled for the future.

      • 4b.2) The use case continues at step 3.

    • etc.

  • ID: UC2

  • Title: Search for events

  • Description: A user searches for multiple existing events.

  • Actor: Any user

  • Main scenario:

    • 1) The user indicates that they want to search for an event.

    • 2) The system displays the event search page.

    • 3) The user optionally specifies a full or partial event name or a date range or both.

    • 4) The system displays all events that satisfy the given conditions.

  • Alternative scenarios:

    • 4a) No events satisfy the conditions.

      • 4a.1) The system warns the user that no such events exist.

      • 4a.2) The use case ends in failure.

Use case specifications can refer back to other use cases if needed.

  • ID: UC3

  • Title: Search for one event

  • Description: A user searches for a single existing event.

  • Actor: Any user

  • Main scenario:

    • 1) The user searches for multiple events (see UC2).

    • 2) The user selects one event from the list.

  • ID: UC4

  • Title: Register for event

  • Description: An account holder signs up for a specific event.

  • Actor: An account holder

  • Main scenario:

    • 1) The user searches for one event (see UC3).

    • 2) The user indicates that they wish to register for that event.

    • 3) The system confirms that the registration was successful.

  • Alternative scenarios:

    • 3b) The event has already ended.

      • 3b.1) The system informs the user that they cannot register for events in the past.

      • 3b.2) The use case continues at step 1.

2. Project Management

2.1. GitHub Project Repository

One of the core components of Agile development is being able to manage the development problem space. GitHub projects extends GitHub’s utility to make problem space management easy.

To create a project, click your user icon and select Your projects from the dropdown menu.

GitProj4

Select the New Project button to create a project. When selecting the template, select Board under the Start from scratch section of the pop-up menu and click Create.

GitProj5

Name your project by selecting the default titular text and replacing it with your own project’s name. This layout is known as Kanban. By default there are three columns: To do, In progress and Done. More columns can be added by clicking the + button at the far right.

GitProj6

Any repository can be added to a project by navigating to the Projects tab of the repository, selecting the Add project button and choosing the desired project from the dropdown menu. Project Kanban boards can be viewed by clicking the Projects tab of the repository and selecting the appropriate Project.

GitHub project cards

To help with better management of the project as you move through project phases, it is prudent to add Milestones. A new milestone can be created by selecting the Issues tab in the repository, and selecting the Milestones tab located next to the New issue button.

To create a new milestone, select the New Milestone button. Then, fill out the form with an appropriate name, due date and description. Once your milestone has been created, you can attach issues to the milestone and see their progress by selecting the Milestones tab. Name your Milestone appropriately, denote the due date and enter a description.

GitHub project milestone creation button

With Milestones and Projects set up, issues can now be assigned to the appropriate project and Milestones. Their status should be changed so they are automatically triaged under the correct Kanban column that matches. As issues are completed and closed, don’t forget to change their status. Closing issues will fill the progress bar on Milestones, while assigning the status of Done shifts issues on the Kanban board.

Note
The status of issues can only be changed after they are created.
GitHub project automation
GitHub project milestone tracking

When creating a new issue it is imperative to be concise but also as descriptive as possible. All the issues you create should have a title, with a comment to describe the issue in detail.

All issues at the time of creation should be assigned to someone. You can always change this later. Label your issues. If none of the default labels fit, new labels can be created to meet your need. This is accomplished by selecting the Labels tab next to the Milestones tab under the Issues section. Then click the New Label button. Finally, assign your issue to the appropriate milestone and project.

For the purpose of tracking progress through the project, never delete issues. Issues should be closed and reopened as needed but never deleted. Even if a mistake was made during creation of an issue, issues can be edited by their creator.

GitHub project issue creation

If you’ve set everything up correctly, your issue board should match your Kanban board. The Kanban board should be a snapshot of how the project is going. Nothing should be done manually here. All the manual labor of opening, moving and triaging issues should be done on the issue board, with automated results appearing on the Kanban board.

GitHub project issue creation

3. Backend

3.1. Setting Up a Local PostgreSQL Database

In this section, we will set up a local PostgreSQL database to store our application’s data.

3.1.1. Installation

Download the latest version of PostgreSQL from https://www.enterprisedb.com/downloads/postgres-postgresql-downloads. Once the download is complete, run the installer. You can stick with the default values for most screens.

Important
For your project, each team member will need to set up their own database on their own machine. You should coordinate with your team members and choose the same port number and password. This will make it easier to configure your app to connect to the database.

The default installation directory should be fine.

PostgreSQL installation: installation directory

Leave every component checked.

PostgreSQL installation: components

The default data directory should be fine.

PostgreSQL installation: data directory

Choose a password. IMPORTANT: do not forget this password. You will need it later.

PostgreSQL installation: password

The default port number should be fine. However, there’s no problem using a different port number in case a different app is using 5432 for some reason. Just remember your choice so that you know which port your app should connect to. Here is a list of typical TCP/UDP ports and their reservation status. Do not use a port that is reserved.

PostgreSQL installation: port number

The default locale should be fine.

PostgreSQL installation: locale

The summary might look something like this:

PostgreSQL installation: summary

There’s no need for other tools, so you can skip the Stack Builder after the installation by unchecking the checkbox.

PostgreSQL installation: skip Stack Builder

Once PostgreSQL is installed, you should be able to connect to your local instance by running the command psql --username postgres and entering your password (I hope you haven’t forgotten it already). If you didn’t use the default port number, you can pass the additional command-line argument --port (e.g., psql --username postgres --port 5433).

Note
If you get an error with some variation of the message "command 'psql' not found," then you likely need to add psql to your PATH environment variable. It should be straightforward to find online instructions to do so on your operating system.

For the course project, each team member will need to set up a separate database instance on their own computer. To simplify configuring your app to connect to the database, each team member should use the same password and port number. If you initially chose different passwords, you can change your password by running psql, running the command \password postgres, and then entering the new password when prompted. You can similarly change the port number (e.g., by following these instructions). In short:

  1. In psql, run the command show config_file; (note the trailing semicolon) to locate the configuration file which stores the port number.

  2. Exit psql.

  3. Open the configuration file, locate the line port = 5432 (where 5432 is replaced by your old port number), change the port number, and save the file.

  4. Restart the PostgreSQL service (or just restart your computer).

3.1.2. Creating a Database

One database management system (in this case, PostgreSQL) can host multiple databases. In psql, create a new database for the event registration app using the command

CREATE DATABASE event_registration;

Check that the database exists by running the command \l:

PostgreSQL create database

3.2. Setting up a Spring Boot Project

We will use the Spring Boot framework to implement the backend of the event registration system. In this section, we will use Spring Initializr to quickly generate the folder structure and some files for a new Spring Boot project.

  1. Go to https://start.spring.io/.

  2. Set the project type to Gradle.

  3. Leave the Spring Boot version at the default value.

  4. Set the names for the group, package, etc.

  5. Set the Java version.

  6. Add the following dependencies:

    1. web

    2. data-jpa

    3. postgresql

Spring Initializr
Note
Since Java version 25 is released on September 16, 2025, you have the option to choose Java 25 for Spring Initializr. However, as of September 24, 2025 (the date where this tutorial was delivered), this is a false claim on their side since Gradle 8.14.3, which is used by Spring Initializr, does not support Java 25. Therefore, if you choose Java 25, you will run into build errors later on. Simply fallback to Java 24 or an earlier version will fix the issue. Eventually Spring Initializr will likely update to Gradle 9.1.0+, which supports Java 25.

Click "GENERATE" and you should get a zip file. Unzip it, move the files into your Git repository, and rename the directory to EventRegistration-Backend. Later, we will add a new directory EventRegistration-Frontend for the user interface code. Your Git repository should look something like this:

.
├── EventRegistration-Backend
│   ├── build.gradle
│   ├── gradle
│   │   └── wrapper
│   │       ├── gradle-wrapper.jar
│   │       └── gradle-wrapper.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── HELP.md
│   ├── settings.gradle
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── ca
│       │   │       └── mcgill
│       │   │           └── ecse321
│       │   │               └── eventregistration
│       │   │                   └── EventregistrationApplication.java
│       │   └── resources
│       │       ├── application.properties
│       │       ├── static
│       │       └── templates
│       └── test
│           └── java
│               └── ca
│                   └── mcgill
│                       └── ecse321
│                           └── eventregistration
│                               └── EventregistrationApplicationTests.java
├── .git
│   └── [omitted...]
├── .gitignore
└── README.md

Open EventRegistration-Backend/src/main/resources/application.properties and write the following configuration information:

spring.datasource.driver-class-name = org.postgresql.Driver

# What to do with existing database tables on startup and shutdown.
# See https://docs.spring.io/spring-boot/how-to/data-initialization.html#howto.data-initialization.using-hibernate.
# ddl-auto=create-drop means all database tables are created on startup and
# dropped (deleted) on shutdown.
# ddl-auto=update does not drop tables on shutdown. It will add new tables and
# columns on startup, but will not delete existing ones.
spring.jpa.hibernate.ddl-auto = update

# Adding the following line leads to better error messages in case the URL or
# credentials are wrong
spring.jpa.database-platform = org.hibernate.dialect.PostgreSQLDialect

# Be careful with the URL format: it is easy to make a typo here
spring.datasource.url = jdbc:postgresql://localhost:5432/event_registration
spring.datasource.username = postgres
spring.datasource.password = PASSWORD

# Decide which port our backend will listen on.
# This is relevant for deliverable 2.
# Could also just set server.port = 8080 to always listen on port 8080.
# The advantage of using the following form is that you can change the port on
# startup, e.g., using .\gradlew bootRun --args='--port=9090'.
server.port = ${port:8080}

(PASSWORD is the password you chose while setting up the local database.)

Warning
For simplicity, we store our local database password directly in the public configuration file. Don’t do this with real credentials. Look for resources on proper secrets management (e.g., https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions).

3.3. The Domain Model

3.3.1. Designing the Model

Before we can program the event registration app, we need to model it.

We clearly need classes representing people and events. We can associate those two classes to keep track of who is registered for each event. It is also standard practice in domain modeling to include a root class that includes all the other classes.

DomainModelNaive

However, this domain model is not suitable for a database.

  1. Since we are only developing one event registration system, the root class is unnecessary. Solution: remove the root class.

  2. Bidirectional associations mean duplicate data in the database; every time someone registers for an event, we would need to add the event to their list of events and add the person to the list of registrants for the event. This is problematic both because (1) it wastes storage space and (2) the data may become inconsistent (what happens if Alice has a certain event in her list of events but Alice is not in the list of registrants for that event?). Solution: make all associations unidirectional.

  3. Many-to-many associations require an extra table in the database. Explicitly representing this as a class in the domain model will make our lives easier. Solution: add a class called "Registration" between User and Event.

  4. In the database, each table needs a primary key column. Solution: in this example, we could theoretically use the email address as a primary key for users and a combination of attributes (e.g., name, date, start time) as the primary key for events. However, it is usually best to simply add an integer ID to be a primary key. Using a single integer is usually simpler than using a combination of attributes and it saves space compared to using a string like an email address or username. Using a meaningful attribute like an email address, name, date, or start time in a primary key also makes it very difficult to change that attribute after the object is created, whereas an integer ID never needs to be changed.

  5. "User" is a reserved keyword in PostgreSQL (https://www.postgresql.org/docs/current/sql-keywords-appendix.html). If we call a database table "User," we will run into syntax errors. Solution: rename the class (to "Person," "Customer," etc.).

DomainModelFixed
Warning
For simplicity, we store users' passwords in plaintext. This is highly insecure. Don’t ever do this with real passwords. See https://www.youtube.com/watch?v=8ZtInClXe1Q.

Note that we need to ensure people cannot register multiple times for the same event twice. There are at least two ways to do this.

  1. Let the primary key for registrations be a "composite key" consisting of the primary keys for the registrant and event. Then, if Alice signs up for the same event twice, there will already be a row in the registrations table with her ID and the event’s ID, and therefore PostgreSQL will prevent the creation of a new row.

  2. In the backend, before creating a new registration, check if the given user is already registered for the given event.

3.3.2. Generating Java Code

Now that we have a good domain model, we translate it to Java code. This can be done by hand or using a tool like Umple. Full documentation on how to use Umple can be found here.

Whether you write the model code by hand or using Umple, create a new package called model under src/main/java/ca/mcgill/ecse321/eventregistration. Each class must have its own file. Make sure your model files declare the package:

package ca.mcgill.ecse321.eventregistration.model;

Your Git repository should now look like this:

.
├── EventRegistration-Backend
│   ├── build.gradle
│   ├── gradle
│   │   └── wrapper
│   │       ├── gradle-wrapper.jar
│   │       └── gradle-wrapper.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── HELP.md
│   ├── settings.gradle
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── ca
│       │   │       └── mcgill
│       │   │           └── ecse321
│       │   │               └── eventregistration
│       │   │                   ├── EventregistrationApplication.java
│       │   │                   └── model
│       │   │                       ├── Event.java
│       │   │                       ├── Person.java
│       │   │                       └── Registration.java
│       │   └── resources
│       │       ├── application.properties
│       │       ├── static
│       │       └── templates
│       └── test
│           └── java
│               └── ca
│                   └── mcgill
│                       └── ecse321
│                           └── eventregistration
│                               └── EventregistrationApplicationTests.java
├── .git
│   └── [omitted...]
├── .gitignore
└── README.md

3.3.3. JPA Annotations

This is an exercise in being able to write JPA compliant code simply by looking at the domain model, which is left up to the students. The documentation for Hibernate 6.5 (the ORM we will be using) can be found here: https://docs.jboss.org/hibernate/orm/6.5/userguide/html_single/Hibernate_User_Guide.html. These are the essential attributes:

  • @Entity: Placed before the class declaration to mark a class as an "entity," which may have a corresponding table in the database. Hibernate has a few requirements for entity classes (e.g., it must have a public, protected, or package-private no-args constructor). Read the docs for more details.

  • @Inheritance(strategy=STRATEGY): Used to specify the inheritance strategy for a class hierarchy, where the superclass is annotated with the @Entity tag. Read the docs for more details.

  • @Id: Placed before the attribute declaration that will serve as the primary unique identifier for the class in the corresponding database table. Read the docs for more details.

  • @EmbeddedId, @Embedded: Used to define a composite primary key. Read the docs for more details.

  • @GeneratedValue(strategy=STRATEGY): Placed between the @Id tag and the attribute declaration, indicating the attribute is to be generated automatically. Read the docs for more details.

  • @OneToOne, @ManyToOne, etc.: Placed before the attribute declaration to specify the multiplicity in associative relationship between the current class and reference class. The first word is the multiplicity of the current class, with the other representing the multiplicity of the other class. Read the docs for more details.

3.3.4. Generating the Database Tables

Once you have added all the required JPA annotations, Hibernate will be able to generate the database tables automatically. From the EventRegistration-Backend/ directory, run ./gradlew test. This will run the default test in EventRegistrationApplicationTests.java. While the test is starting, Hibernate will create the database. After the test passes, connect to the database using psql --username postgres --dbname event_registration and list the tables using \dt. There should be one table per model class.