We would like to thank our TA Nigel Yeo, Prof Akshay and the CS2113 Team for their guidance.
The Internity application adopts layered architecture where responsibilities are divided among UI, Logic, Model and Storage related components. It also follows the Command Pattern for handling user actions. This design separates concerns clearly, allowing for modular, maintainable and extensible code.
The Architecture Diagram below explains the high-level design of the Internity application.

The diagram below shows a simplified Class Diagram of all of Internity’s classes and their relationships.
(Ui class omitted for simplification as most classes are dependent on it.)

InternityManagerLogic layer.Logic layer may modify the Model or trigger UI updates.Storage layer.Ui, DashboardUiLogic or Model layers in a user-friendly format.CommandParser, CommandFactory, ArgumentParser, Command subclassesModel operations.Command object through the CommandFactory.Model or trigger the UI to display information.InternshipList, Internship, Date, StatusStorageThe Sequence Diagram below shows a simplified version of how the components interact with each other when the user issues the command
delete 1.

The UI component is responsible for all interactions between the user and the application. It displays messages, prompts, and formatted lists in the command-line interface (CLI), and ensures that feedback from executed commands is presented clearly.
The API of this component is specified in the Ui.java class
and the DashboardUi.java.

InternityManager handles all user input through a Scanner.
When a command is executed, it delegates output responsibilities to the Ui class.Ui component formats and prints the messages or internship data to the console.
For example:
Ui.printAddInternship() displays confirmation for a newly added internship.Ui.printFindInternship() displays results in a neat, column-aligned format.DashboardUi class is used.Ui class methods are static to ensure simplicity and easy access across commands without requiring
instantiation.The Logic component is responsible for:
Command object.This component follows the Command Pattern, which decouples the user input parsing from the execution of commands.
Each command is represented as a subclass of the abstract Command class, encapsulating its execution logic. This
allows new commands to be easily added without modifying the core parsing or execution workflow.

The class diagram above shows the main classes involved in parsing, creating, and executing commands.
add company/Google role/Software Engineer deadline/17-09-2025 pay/7000) is received by CommandParser.CommandParser:
CommandFactory.CommandFactory:
Command subclass (e.g. AddCommand).ArgumentParser to interpret argument strings.Command object.Command object executes its logic (e.g. adds a new internship to InternshipList).Ui.The following sequence diagram illustrates how the Logic Component processes an input command:

add, delete, find, list, update, usernameadd needs company, role, deadline and pay.update needs an index and fields to update.find needs a search keyword.dashboard, exit, helpCommandFactory directly constructs the corresponding Command object (e.g. ExitCommand or DashboardCommand) without invoking ArgumentParser.This distinction is represented in the above sequence diagram’s alt block, showing the two conditional flows:
ArgumentParser, then instantiated.API: internity.core
The Model component:
Internship objects in an InternshipList objectadd, delete, update, find, list internshipsUI, Logic, Storage)
The class diagram above shows the main classes involved in manipulating Internship objects.
Getters and setters have been omitted from Class Diagram for clarity.
The following sequence diagram illustrates how the Model Component processes an AddCommand:

The sequence diagram above shows how the AddCommand interacts with the InternshipList to add a new internship.
InternshipList.add() method is called with the necessary parameters (company, role, deadline, pay).InternshipList creates a new Internship object with those parameters.InternshipList calls add() to add the new internship to the static ArrayList of internships.The following object diagram illustrates the objects after an internship has been added using the AddCommand.

After executing the AddCommand, a new Internship object is created and added to the InternshipList.
The Internship object contains all relevant attributes, including company, role, pay, status, and a deadline
that points to a separate Date object representing the day, month and year of the application deadline.
The AddCommand maintains a reference to both the newly created Internship and its associated Date during execution.
The object diagram illustrates these relationships, showing that InternshipList now contains the new Internship, and
that deadline is a distinct object referenced by Internship.
The following sequence diagram illustrates how the Model Component processes an Update command:

The sequence diagram above shows how the UpdateCommand interacts with the InternshipList to update an existing internship.
InternshipList.update() method is called with the index of the internship to update and the new status.InternshipList retrieves the existing Internship object at that index.InternshipList calls the setStatus() method on the Internship object to update its status.API: Storage.java
The Storage component:
internity.core package (specifically Internship and InternshipList) to load and save internship data.The following class diagram shows the Storage component and its relationships:

The class diagram illustrates:
API: AddCommand.java
The add mechanism allows users to record new internship entries in their tracking list. This feature ensures users can maintain a comprehensive and organised list of upcoming internship opportunities, along with key details such as company, role, deadline and pay.
The add mechanism is implemented by the AddCommand class, which extends the abstract Command class. It encapsulates the logic for validating user input, constructing an Internship object and inserting it into the internship list.
Key components involved:
AddCommand — Encapsulates the creation and insertion of a new internship entryArgumentParser.parseAddCommandArgs() — Parses and validates raw user input into individual internship fieldsInternshipList.add() — Inserts the constructed internship into the static internship listUi.printAddInternship() — Displays a confirmation message with internship detailsThe following sequence illustrates how the add command is processed from user input to data persistence.
Step 1. The user launches the application and executes a command such as:
add company/Google role/Software Engineer deadline/17-09-2025 pay/7000
update command to change the status.Step 2. The CommandParser splits the input into command word "add" and the argument string
"company/Google role/Software Engineer deadline/17-09-2025 pay/7000".
Step 3. The CommandFactory delegates parsing to ArgumentParser.parseAddCommandArgs(args), which performs detailed extraction and validation of all fields.
The ArgumentParser.parseAddCommandArgs() method converts a raw argument string into a valid AddCommand instance.
This process ensures strict input integrity, correct field representation and data consistency.
Parsing process:
1. Input Validation
InternityException.invalidAddCommand() if the check fails.2. Splitting Fields
PARSE_LOGIC_ADD, which employs a lookahead to separate fields at whitespace positions that precede one of the expected prefixes (company/, role/, deadline/, or pay/).company/, role/, deadline/ and pay/, and in this exact order.InternityException.invalidAddCommand().InternityException.noFieldForAdd() specifying that field.3. Extracting Values
company/Google → "Google"
role/Software Engineer → "Software Engineer"
deadline/17-09-2025 → "17-09-2025"
pay/7000 → "7000"
company, role, deadlineString, payString.4. Field Lines and Emptiness Checks
InternityException.emptyField() otherwise.Ui.COMPANY_MAXLENUi.ROLE_MAXLENInternityException.exceedFieldLength().5. Data Conversion
deadlineString into a Date object via DateFormatter.parse().payString into an integer using Integer.parseInt().
payString contains non-numerical character(s), is negative or exceeds Integer.MAX_VALUE, throws InternityException.invalidPayFormat().6. Command Construction
AddCommand instance is created:
return new AddCommand(company, role, deadline, pay);
At any failure stage: The issue is logged an appropriate InternityException is thrown to provide clear user feedback.
Step 4. When InternityManager calls AddCommand.execute():
Internship object is created using the parsed details.InternshipList.add(internship) method adds the internship to the global static list.Ui.printAddInternship() method displays a formatted confirmation message to the user. Added this internship:
Company: Google
Role: Software Engineer
Deadline: 17-09-2025
Pay: 7000
Status: Pending
Now you have 1 internship(s) in the list.
Step 5. After execution, InternityManager triggers InternshipList.saveToStorage(), which internally calls Storage.save() to persist the updated internship list to disk.
| Step | Component | Action |
|---|---|---|
| 1 | User | Inputs add company/Google role/Software Engineer deadline/17-09-2025 pay/7000 |
| 2 | CommandParser | Separates command and arguments |
| 3 | ArgumentParser | Parses and validates all four fields |
| 4 | AddCommand | Creates Internship object and calls InternshipList.add() |
| 5 | Ui | Displays success message |
The following sequence diagram illustrates the complete add operation flow:

Aspect: Inputting parameters by prefix
company/, role/, deadline/, pay/
c/, r/, d/, p/
Aspect: Status field
update command.Aspect: Order of parameters
Aspect: Allowing duplicate internship entries
Rationale:
Aspect: Allowing any deadline for internship entries
Rationale:
API: UpdateCommand.java
The update mechanism lets users modify one or more fields of an existing internship entry. It keeps the list accurate as applications evolve, without the need for users to re-enter the whole record.
UpdateCommand extends the abstract Command class. Users specify a 1-based index in the CLI, which is converted to a 0-based index during parsing. Any subset of fields can be provided. Only non-null fields are applied.
Key components involved
UpdateCommand Encapsulates the multi-field update with guarded calls for each optional field and a final success message.ArgumentParser.parseUpdateCommandArgs() Parses update INDEX [company/...] [role/...] [deadline/...] [pay/...] [status/...], converts 1-based index to 0-based, validates tags and formats, constructs UpdateCommand.InternshipList.updateCompany() Sets company after index bounds check.InternshipList.updateRole() Sets role after index bounds check.InternshipList.updateDeadline() Sets deadline after index bounds check.InternshipList.updatePay()Sets pay after index bounds check and non-negative parsing in the parser.InternshipList.updateStatus()Validates and normalizes status, then sets it after index bounds check.Ui.printUpdateInternship() Confirms a successful update to the user.Given below is an example usage scenario and how the update mechanism behaves at each step.
InternshipList. The user executes:
update 1 company/Google role/Software Engineer pay/9000
Step 2. Parsing input
CommandParser receives the input and splits it into command word update and the remaining arguments.
Step 3. Creating the command
CommandFactory delegates to ArgumentParser.parseUpdateCommandArgs(...), which:
company/, role/, deadline/, pay/, status/ using the predefined regular expression pattern PARSE_LOGIC_UPDATE.deadline/ is parsed with DateFormatter.parse(...).pay/ is parsed as a non-negative integer.status/ must be non-empty and is later normalized by InternshipList.updateStatus.new UpdateCommand(index, company, role, deadline, pay, status)
Step 4. Executing the command
InternityManager calls UpdateCommand.execute(), which:
isUpdated = false.InternshipList.updateX(...).updateStatus additionally validates and canonicalizes the status string.InternityException with a clear message.Ui.printUpdateInternship() to acknowledge the update.
update arguments → ArgumentParser.invalidUpdateFormat()InternityException.noUpdateFieldsProvided()InternityException.invalidIndexForUpdate()InternityException.unknownUpdateField(...)pay → InternityException.invalidPayFormat()InternshipList.updateX throws InternityException.invalidInternshipIndex()status → InternityException.invalidStatus(...) or InternityException.emptyField("status/")update 3 status/Interviewing # Update only status
update 2 company/Apple role/ML Engineer # Update company and role
update 4 deadline/15-12-2025 pay/8500 # Update deadline and pay
update 1 # Invalid - no fields
update status/Accepted # Invalid - no internship index
Ui for consistent output formatting.API: DeleteCommand.java
The delete mechanism allows users to remove internship entries from their tracking list. This feature is essential for maintaining an up-to-date list of relevant internship applications by removing entries that are no longer needed.
The delete mechanism is facilitated by DeleteCommand, which extends the abstract Command class. It stores an index field internally as a 0-based integer, although users interact with 1-based indices for a more natural experience.
Key components involved:
DeleteCommand - Encapsulates the delete operation with validation and execution logicArgumentParser.parseDeleteCommandArgs() - Converts user input to a 0-based indexInternshipList.delete() - Removes the internship from the static list with bounds checkingInternshipList.get() - Retrieves internship details before deletion for user feedbackStorage.save() - Automatically persists changes after successful deletionGiven below is an example usage scenario and how the delete mechanism behaves at each step.
Step 1. The user launches the application and the InternshipList contains 3 internships. The user executes delete 2 to delete the 2nd internship in the list.
Step 2. The CommandParser validates the input and splits it into command word "delete" and arguments "2".
Step 3. The CommandFactory delegates to ArgumentParser.parseDeleteCommandArgs("2"), which:
"2" as an integerDeleteCommand(1)Step 4. InternityManager calls DeleteCommand.execute(), which:
InternshipList.get(1) to retrieve the internship details (for displaying to the user)InternshipList.delete(1) to remove the internship from the list
InternshipList.size() to get the updated list sizeUi.printRemoveInternship() to display a confirmation messageStep 5. After the command completes, InternityManager automatically calls InternshipList.saveToStorage(), which in turn calls Storage.save() to persist the changes to disk.
The following sequence diagram illustrates the complete delete operation flow:

The sequence diagram shows how the delete command flows through multiple layers:
InternityManagerCommandParser and CommandFactory work with ArgumentParser to create the commandDeleteCommand interacts with InternshipList and UiStorageAPI: ListCommand.java
The list mechanism is implemented by the ListCommand class, which allows users to view all internships in their list.
Below is the sequence diagram for a common usage of the list feature:

ListCommand accesses the InternshipList, which contains the ArrayList<Internship> of all stored internships.sort/asc is specified, InternshipList.sortInternships(order) returns a new ArrayList copy sorted by deadline in ascending order. The original list is not modified.sort/desc is specified, InternshipList.sortInternships(order) returns a new ArrayList copy sorted by deadline in descending order. The original list is not modified.Ui.printList().Aspect: Index base convention
ArgumentParser.
Aspect: When to retrieve internship details
get() call before deletionAspect: Index validation location
InternshipList.delete() and InternshipList.get().
get() and delete() are calledArgumentParser before creating DeleteCommand.
API: FindCommand.java

The find mechanism is implemented by the FindCommand class, which allows users to search for internships based on a
keyword that matches either the company name or the role of the internship.
The FindCommand class extends Command and consists of the following key components and operations:
FindCommand ConstructorFindCommand object with a given keyword. public FindCommand(String keyword);
keyword: The keyword to search for in the company or role fields of the internships.FindCommand.execute() @Override
public void execute() throws InternityException;
InternshipList.findInternship(keyword) to filter and search through the list of internships.InternshipList.findInternship() public static void findInternship(String keyword);
keyword: The string to search for in the company or role names of internships.keyword.toLowerCase()).Ui.printFindInternship() method for display.Step 1: The user launches the application and executes a find command by typing find Google.
Step 2: The CommandParser parses the input, extracting the command word find and the argument Google.
Step 3: The CommandFactory creates a FindCommand with the keyword Google.
Step 4: The FindCommand.execute() method is called, triggering the InternshipList.findInternship() method.
Step 5: findInternship() filters the internships, looking for the keyword in both the company and role fields.
If any matches are found, they are displayed through the UI.
Step 6: If no matches are found, the user sees the message printed in the UI: “No internships with this company or role found.”
Keyword Matching: The keyword is matched against both the company and role fields of each internship in a
case-insensitive manner using the toLowerCase() method.
Logging: The command execution is logged at the start and end, using the Logger class to track the command’s
lifecycle.
UI Handling: When matching internships are found, they are passed to the Ui.printFindInternship() method
for display. The UI is responsible for presenting the search results to the user.
find Google
These are the matching internships in your list:
_____________________________________________________________________________________________________________
No. Company Role Deadline Pay Status
_____________________________________________________________________________________________________________
1 Google Software Engineer 17-09-2025 120000 Pending
2 Alphabet Googleerrr 17-09-2025 120000 Pending
_____________________________________________________________________________________________________________
If no internships match:
No internships with this company or role found.
Case Insensitivity: The search is case-insensitive, so find google, find GOOGLE, or find GoOgLe
would all match the same internships.
Empty or Invalid Keyword: If an empty string is provided as the keyword, the Ui will print “Invalid find command. Usage: find KEYWORD”
Performance: The search mechanism uses a stream-based filter on the internship list, which is efficient for moderate-sized datasets but may require optimisation for larger datasets.
Aspect: Filtering criteria
company or role field.
find company/Google or find role/Engineer).
Aspect: Matching behavior
.toLowerCase().contains().API: UsernameCommand.java
The Username feature allows the user to set a personalised username that is stored within the application’s persistent data model and displayed in future interactions.

UsernameCommand is created by the CommandParser after recognising the username keyword from user input.
username Jane Doe
UsernameCommand constructor validates that the argument is non-null and non-blank.execute() is called:
InternshipList.setUsername(username).Ui.printSetUsername(username) to show the change.Ui.printSetUsername() provides clear confirmation of a successful command execution.API: DashboardCommand.java
The Dashboard feature presents a comprehensive summary of the user’s internship tracking data, including the username, total internships, status overview and nearest deadline.

DashboardCommand serves as a simple trigger to call the UI layer.
dashboard
DashboardUi class handles all the logic for displaying information retrieved from InternshipList.DashboardUi.printDashboard(), the following occurs:
InternshipList.getUsername().InternshipList.size().InternshipList.getNearestDeadline().
(OVERDUE!).DashboardCommand delegates all display logic to DashboardUi.DashboardUI delegates all data retrieval logic to InternshipList.DashboardUi class can easily be expanded to include additional statistics in the future.API: ExitCommand.java
The ExitCommand allows the user to gracefully terminate the Internity application. Upon execution, it ensures that the user is notified and the main command loop in InternityManager is stopped.

exit, the CommandParser returns an ExitCommand instance.InternityManager calls execute() on the command, which triggers an interaction with the Ui class to display a friendly exit message before
termination.isExit() returns true, the main loop breaks, ending the program.Command subclass that returns true for isExit().API: HelpCommand.java
The HelpCommand provides a quick reference to all available commands in the Internity application.
The commands are listed in an ordered, easy-to-read format to assist navigation and usage.

help, the CommandParser returns a HelpCommand instance.InternityManager calls execute() on the command, which invokes the Ui.printHelp() method.Ui.printHelp() prints a formatted list detailing all commands.API: Storage.java
The Storage feature provides persistent data storage for Internity, allowing users to save their internship data across application sessions. Without this feature, users would lose all their internship data upon closing the application. This is a critical feature that transforms Internity from a temporary session-based tool to a reliable long-term tracking system.
The Storage mechanism uses a human-readable, pipe-delimited text file format that can be easily inspected and manually edited if needed. The implementation is split between the Storage class (which handles file I/O) and InternshipList (which coordinates the loading and saving operations).
Key components involved:
Storage - Handles all file I/O operations, parsing, validation, and formattingInternshipList.loadFromStorage() - Coordinates the loading process during application startupInternshipList.saveToStorage() - Coordinates the saving process after each commandInternityManager - Calls load on startup and auto-saves after each command executionDateFormatter - Parses date strings during loadingInternityException - Signals storage-related errorsFile format specification:
Data is stored in a single file at ./data/internships.txt with both username and internships.
Username (in line below):
<username>
<company> | <role> | <DD-MM-YYYY> | <pay> | <status>
<company> | <role> | <DD-MM-YYYY> | <pay> | <status>
...
Example:
Username (in line below):
John Doe
Google | Software Engineer | 15-12-2025 | 5000 | Pending
Meta | Data Analyst | 20-01-2026 | 4500 | Applied
Amazon | Backend Developer | 10-11-2025 | 6000 | Interview
The load operation occurs once during application startup, before the user sees the welcome message.
Step 1. InternityManager.start() calls InternityManager.loadData() which invokes InternshipList.loadFromStorage().
Step 2. InternshipList.loadFromStorage() calls Storage.load(), which returns an ArrayList<Internship>.
Step 3. Inside Storage.load():
BufferedReader to read the file line by line."Username (in line below):").InternshipList.setUsername().parseInternshipFromFile() to parse the line."|" delimiter and trim all parts.parseAndValidateFields() to validate each field:
Internship object and add it to the list.Step 4. Storage.load() returns the ArrayList of successfully parsed internships.
Step 5. InternshipList.loadFromStorage() clears the static list and adds all loaded internships.
The following sequence diagram illustrates the load operation:




The load sequence diagram demonstrates the robust error-handling approach: corrupted lines are skipped with warnings rather than causing the entire load operation to fail. This design choice prioritizes availability over strict consistency, ensuring users can still access their valid data even if some entries are corrupted.
The save operation occurs automatically after every command that modifies data (add, delete, update, username).
Step 1. After a command completes execution, InternityManager calls InternityManager.saveData(), which invokes InternshipList.saveToStorage().
Step 2. InternshipList.saveToStorage() calls Storage.save(List), passing the static ArrayList.
Step 3. Inside Storage.save():
PrintWriter to write to a temporary file."Username (in line below):").InternshipList.getUsername().formatInternshipForFile() to create the pipe-delimited string."company | role | DD-MM-YYYY | pay | status".PrintWriter to flush and finalize the file.Step 4. If any IOException occurs, wrap it in an InternityException and throw it. InternityManager catches this and displays a warning (but doesn’t crash the application).
The following sequence diagram illustrates the save operation:

The save sequence diagram shows the straightforward serialization process. Note that the entire file is rewritten on each save operation, which is acceptable for the target use case (up to 1000 internships) but would require optimization for larger datasets.
To ensure data integrity during save operations, the Storage feature employs a strategy of writing to a temporary file followed by an atomic move to replace the original file. This approach minimizes the risk of data corruption in case of application crashes or interruptions during the write process.
If the atomic move fails (e.g., due to filesystem limitations), the system falls back to a standard overwrite method while logging a warning.
Aspect: File format choice
ObjectOutputStream.
Aspect: Error handling strategy
Aspect: When to save
Internity provides a centralized and efficient way to manage internship applications through a command-line interface. It allows users to:
| Version | As a … | I want to … | So that I can … |
|---|---|---|---|
| v1.0 | new user | add a new internship with company, role, and deadline details | keep all opportunities organized in one place |
| v1.0 | user | set the status of my application (applied, interview, offer, rejected) | easily see my progress with each internship |
| v1.0 | user | see a list of all my internships | easily view the opportunities I’m tracking |
| v1.0 | user | remove an internship entry | keep the list relevant and up to date |
| v2.0 | user | update the company, role, deadline and pay for an internship | keep my application information accurate and up to date |
| v2.0 | user | see my internships sorted by deadlines | prioritize applications that are due soon |
| v2.0 | user | save and load internships automatically | avoid losing my progress and notes between sessions |
| v2.0 | user | find internships based on the company or role | easily view my applications to specific companies or positions I’m interested in |
| v2.0 | user | set or change my username | personalize my internship tracker experience |
| v2.0 | user | view a condensed dashboard | to see the current status of my applications |
| v2.0 | new user | I can view an overview of the list of commands | so that I can access the possible commands more conveniently |
17 or above installed.execute(), isExit()).Internship
A temporary work experience offered by a company or organization that allows a student or early-career individual
to gain practical skills, industry knowledge and professional exposure in a specific field. Internships may be paid
or unpaid, part-time or full-time, and can occur during or after academic study.
Given below are instructions to test the app manually.
Test case 1: Add a valid internship
add company/Microsoft role/Intern deadline/15-12-2025 pay/5000Test case 2: Add an internship with missing fields
add company/Microsoft role/InternTest case 3: Add an internship with invalid pay
add company/Bay Harbour role/Butcher deadline/15-12-2025 pay/-1000Test case 4: Add an internship with an empty field
add company/Uber role/ deadline/15-12-2025 pay/2000Prerequisite: Have one internship added to the list.
Test case 1: Update a single field (company name)
update 1 company/Apple.Test case 2: Update multiple fields (company, role, and pay)
update 1 company/Tesla role/ML Engineer pay/10000Test case 3: Invalid index
update 1000 company/NetflixTest case 4: Missing update fields
add company/Meta role/Designer deadline/05-11-2025 pay/6000.update 1.Prerequisite: At least one internship has been added.
Test case 1: Delete an internship by index
delete 1Test case 2: Delete with invalid index (as there are fewer than 1000 internships in the list)
delete 1000Invalid internship index: 1000Test case 3: Delete with index 0 or negative index
delete 0, delete -1Invalid internship index: 0 or Invalid internship index: -1Test case 1: List all internships in the order they were added
list.Test case 2: List all internships sorted by deadline ascending
list sort/asc.Test case 3: List all internships sorted by deadline descending
list sort/desc.Prerequisites: At least one internship has been added.
Test case 1: Find by company name
find MicrosoftMicrosoft (case-insensitive) are displayed.Test case 2: Find by role name
find InternIntern (case-insensitive) are displayed.Prerequisites: The application has been launched and the user is at the command prompt.
Test case 1: Changing username I
username DexterUsername set to Dexter.Test case 2: Changing username II
username Dexter <3 NicoleUsername set to Dexter <3 Nicole.Test case 3: Invalid username input
username (without specifying a name)Test case 1: Display dashboard with multiple internships
dashboardTest case 2: Display dashboard with no internships
dashboardTest case 3: Display dashboard after changing username
username Doakes, then dashboardDoakes.Test case 4: Dashboard reflects recent changes
dashboardTest case 5: Nearest deadline is overdue
dashboard(OVERDUE!).Test case 6: Multiple internships with the same nearest deadline
dashboardPrerequisites:
Test case 1: Save after adding internships
Test case 2: Save after updating an internship
Test case 3: Save after deleting an internship