Build a CLI CRUD App in Pure Java
Build a Java inventory manager with ANSI colors, CSV persistence, and table formatting.
Introduction
30 Second Summary
Every application that manages data needs a way to create, view, update, and delete records. From the inventory systems in retail stores to the admin panels behind your favorite apps, this pattern shows up everywhere.
In this project, you will build a command-line Item Inventory Manager using pure Java. You will store data in a CSV file, display it in professionally formatted tables, and add colored terminal output for a polished user experience.
What You'll Build
A CLI application that performs full CRUD operations on an inventory of items, with MySQL-style table formatting and ANSI color-coded output.
By the end of this project, you'll have:
- A working CLI inventory manager that creates, reads, updates, and deletes items stored in a local CSV file.
- A reusable TableFormatter utility class that renders data in MySQL-style bordered tables with perfectly aligned columns.
- A Color utility class using ANSI escape codes that produces green success messages, red error messages, cyan headers, and yellow prompts in the terminal.
- Secret Mission: Add search and filter functionality so users can find items by name or category with highlighted matching text.
Are there any prerequisites?
This project assumes you have basic Java syntax knowledge (variables, methods, classes, loops, and conditionals). You should already have JDK 21 and IntelliJ IDEA installed on your machine.
Set Up the Project
Before you can build your inventory manager, you need a properly structured Java project. IntelliJ IDEA handles compilation, running, and file management for you, so getting the project set up correctly now saves headaches later.
In this step, you will create a new plain Java project in IntelliJ IDEA, set up your package structure, and scaffold the five Java files that make up the application.
In this step, get ready to:
- Create a new Java project in IntelliJ IDEA with the IntelliJ build system and JDK 21.
- Create the inventory package and verify compilation with a temporary test.
- Scaffold all five Java files that form the application structure.
Create a new Java project in IntelliJ IDEA
The project uses IntelliJ's native build system instead of Maven or Gradle. This keeps things lightweight since the application uses only built-in JDK libraries.
- Press the Super key to open the Activities overview, type IntelliJ, and click the IntelliJ IDEA icon to launch it.
- On the Welcome screen, click New Project.
- In the New Project wizard, select Java from the list on the left.
- In the Name field, type inventory-manager.
- Set Build system to IntelliJ.
- In the JDK dropdown, select your installed JDK 21.
- Make sure Add sample code is unchecked.
- Click Create.
IntelliJ IDEA creates the project and opens it. You should see the Project tool window on the left showing a src folder.
Why the IntelliJ build system?
IntelliJ's native builder compiles your Java code without needing Maven or Gradle configuration files. Since this project uses only JDK built-in libraries, a full build tool would add unnecessary complexity.
- Now create the package structure. In the Project tool window, right-click the src folder.
- Select New, then Package.
- Type inventory and press Enter.
What is a Java package?
A package groups related classes together and prevents naming conflicts. Every file in the inventory package starts with package inventory; at the top, and IntelliJ adds this automatically when you create classes inside the package.
Don't see the src folder?
If the Project tool window is not visible, press Alt+1 to toggle it open. If src is missing, the project may have been created as an Empty Project instead of a Java project. Start over and make sure you select Java in the left panel of the New Project wizard.
Get help from the NextWork AI assistant: My IntelliJ project doesn't have a src folder after creation.
Create the Color utility file and verify compilation
The Color.java file will eventually hold ANSI escape code constants and helper methods for colored terminal output. For now, you will create it as a stub and add a temporary main method to confirm your project compiles and runs correctly.
- In the Project tool window, right-click the inventory package.
- Select New, then Java Class.
- Type Color and press Enter.
IntelliJ generates the file with the package declaration and an empty class body. Now add a temporary main method inside the class to test that everything works.
- Replace the contents of Color.java with this temporary test code:
package inventory;
public class Color {
// Temporary main method to verify the project runs
public static void main(String[] args) {
System.out.println("\u001B[32mSetup works!\u001B[0m");
}
}
What does this code do?
This is a throwaway test to confirm IntelliJ can compile and run your code. The string \u001B[32m is an ANSI escape code that turns terminal text green. The string \u001B[0m resets the color back to normal. If you see green text in the output, your terminal supports ANSI codes and your project is wired up correctly.
- Click the green Run icon in the gutter (the left margin) next to the main method.
- Select Run 'Color.main()' from the popup.
The Run panel opens at the bottom of IntelliJ. You should see Setup works! printed in green text.
Seeing a compilation error?
Check that the first line of Color.java says package inventory; exactly. The package declaration must match the folder the file lives in.
If you see a red underline on the JDK imports, go to File > Project Structure > Project and confirm the SDK is set to 21.
Get help: Why is my Color.java not compiling in IntelliJ?.
- Now remove the temporary main method. Replace the contents of Color.java with just the empty class stub:
package inventory;
public class Color {
}
- Save the file with Ctrl+S.
Why remove the temporary code?
You confirmed that IntelliJ, JDK 21, and ANSI color output all work. The real implementation of Color.java comes in the next step. Removing the temp code keeps your files clean and ready for the actual implementation.
Create the remaining project files
The application is split into five classes, each with a single responsibility. You already have Color.java. Now create the other four.
- Right-click the inventory package in the Project tool window.
- Select New > Java Class.
- Type TableFormatter and press Enter.
- Repeat the same process to create Item.
- Create ItemRepository.
- Create InventoryApp.
What are these five files for?
- Color.java handles colored terminal output using ANSI escape codes.
- TableFormatter.java renders data in MySQL-style bordered tables.
- Item.java defines the data model for an inventory item.
- ItemRepository.java reads and writes items to a CSV file.
- InventoryApp.java contains the main menu loop and CRUD logic.
- Verify the project compiles by going to Build > Build Project in the top menu (or press Ctrl+F9).
You should see no errors in the Build output panel at the bottom. Your Project tool window should show five files inside src/inventory/.
Build shows an error?
Make sure every file's first line is package inventory;. IntelliJ adds this automatically when you create a class inside the inventory package, but if you accidentally created a file at the wrong level, the package line may be missing.
Get help: My project has build errors after creating the class files.
✔️ Awesome, I've got everything!
Great. Double check you have saved all five files and the build succeeds with no errors.
ⓧ I'd like to double check the full code
Here is what each file should look like at this point.
package inventory;
public class Color {
}
package inventory;
public class TableFormatter {
}
package inventory;
public class Item {
}
package inventory;
public class ItemRepository {
}
package inventory;
public class InventoryApp {
}
Your project skeleton is ready. Next up, you will bring Color.java and TableFormatter.java to life with ANSI escape codes and MySQL-style table formatting.
Build the Color Utility and Table Formatter
Your project structure is set up and all five Java files are in place. Now it's time to give your application its visual personality.
In this step, you will build two utility classes that the rest of the app depends on. Color.java wraps ANSI escape codes into simple helper methods so you can print colored text in the terminal. TableFormatter.java renders data in MySQL-style bordered tables with perfectly aligned columns.
In this step, get ready to:
- Implement the Color utility class with ANSI constants and helper methods.
- Build a TableFormatter that renders bordered tables with dynamic column widths.
- Test both classes together to confirm colored output and table alignment.
Implement the Color utility class
Terminal colors in Java work by printing special character sequences before and after your text. These sequences are invisible to the user but tell the terminal to switch colors.
- In IntelliJ, open src/inventory/Color.java from the Project tool window on the left.
- Replace the entire file contents with the following code:
package inventory;
public class Color {
// ANSI escape code constants
public static final String RESET = "\u001B[0m";
public static final String RED = "\u001B[31m";
public static final String GREEN = "\u001B[32m";
public static final String YELLOW = "\u001B[33m";
public static final String CYAN = "\u001B[36m";
public static final String BOLD = "\u001B[1m";
// Prints a green success message
public static void success(String msg) {
System.out.println(GREEN + msg + RESET);
}
// Prints a red error message
public static void error(String msg) {
System.out.println(RED + msg + RESET);
}
// Prints a yellow prompt without a newline
public static void prompt(String msg) {
System.out.print(YELLOW + msg + RESET);
}
// Prints a bold cyan header
public static void header(String msg) {
System.out.println(CYAN + BOLD + msg + RESET);
}
}
What does this code do?
Each constant like RED holds an ANSI escape sequence that tells the terminal to switch to that color. RESET turns formatting back to normal after the colored text.
The helper methods wrap a message between a color code and RESET so you never have to remember the escape sequences yourself. Notice that prompt() uses System.out.print (no newline) so the user's input appears on the same line.
- Save Color.java by pressing Cmd+S (macOS) or Ctrl+S (Windows/Linux).
- Confirm IntelliJ shows no red underlines in the editor. A clean file means the class compiles without errors.
Seeing red underlines?
Make sure the first line says package inventory; exactly. If the package declaration is missing or misspelled, IntelliJ cannot resolve the class within your project structure.
Also check that the file is inside the src/inventory/ directory, not directly under src/.
help me fix red underlines in my Color.java file
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
package inventory;
public class Color {
public static final String RESET = "\u001B[0m";
public static final String RED = "\u001B[31m";
public static final String GREEN = "\u001B[32m";
public static final String YELLOW = "\u001B[33m";
public static final String CYAN = "\u001B[36m";
public static final String BOLD = "\u001B[1m";
public static void success(String msg) {
System.out.println(GREEN + msg + RESET);
}
public static void error(String msg) {
System.out.println(RED + msg + RESET);
}
public static void prompt(String msg) {
System.out.print(YELLOW + msg + RESET);
}
public static void header(String msg) {
System.out.println(CYAN + BOLD + msg + RESET);
}
}
Build the table formatter
A well-formatted table needs dynamic column widths. Each column must be as wide as its longest value, with consistent borders built from +, -, and | characters.
- Open src/inventory/TableFormatter.java in IntelliJ.
- Replace the entire file contents with the following code:
package inventory;
import java.util.List;
public class TableFormatter {
// Public entry point: prints a bordered table to the terminal
public static void printTable(String[] headers, List<String[]> rows) {
// Step 1: Calculate the maximum width for each column
int[] colWidths = new int[headers.length];
for (int i = 0; i < headers.length; i++) {
colWidths[i] = headers[i].length();
}
for (String[] row : rows) {
for (int i = 0; i < row.length; i++) {
// Use visible length to ignore ANSI escape codes in width calculation
int visibleLength = stripAnsi(row[i]).length();
if (visibleLength > colWidths[i]) {
colWidths[i] = visibleLength;
}
}
}
// Step 2: Print the table with borders
String separator = buildSeparator(colWidths);
System.out.println(separator);
System.out.println(buildRow(headers, colWidths));
System.out.println(separator);
for (String[] row : rows) {
System.out.println(buildRow(row, colWidths));
}
System.out.println(separator);
}
// Builds a separator line like +----+--------+
private static String buildSeparator(int[] colWidths) {
StringBuilder sb = new StringBuilder("+");
for (int width : colWidths) {
sb.append("-".repeat(width + 2));
sb.append("+");
}
return sb.toString();
}
// Builds a data row like | ID | Name |
private static String buildRow(String[] values, int[] colWidths) {
StringBuilder sb = new StringBuilder("|");
for (int i = 0; i < values.length; i++) {
String value = values[i];
int visibleLength = stripAnsi(value).length();
int padding = colWidths[i] - visibleLength;
sb.append(" ");
sb.append(value);
sb.append(" ".repeat(padding + 1));
sb.append("|");
}
return sb.toString();
}
// Removes ANSI escape codes so we can measure visible text length
private static String stripAnsi(String text) {
return text.replaceAll("\u001B\\[[;\\d]*m", "");
}
}
What does this code do?
printTable() first measures every header and row value to find the widest entry in each column. It then prints a bordered table using that width information.
buildSeparator() produces lines like +----+--------+ by repeating dashes to match each column width. buildRow() pads each cell value with spaces so all columns align perfectly.
stripAnsi() is critical for the Secret Mission later. ANSI color codes add invisible characters to strings. Without stripping them, the table would miscalculate column widths when cells contain colored text.
- Save TableFormatter.java.
- Confirm IntelliJ shows no red underlines. The import java.util.List at the top should resolve without errors.
Import not resolving?
If List shows as red, check that your project SDK is set to JDK 21. Go to File then Project Structure and confirm the Project SDK field shows 21.
help me fix unresolved imports in my TableFormatter.java file
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
package inventory;
import java.util.List;
public class TableFormatter {
public static void printTable(String[] headers, List<String[]> rows) {
int[] colWidths = new int[headers.length];
for (int i = 0; i < headers.length; i++) {
colWidths[i] = headers[i].length();
}
for (String[] row : rows) {
for (int i = 0; i < row.length; i++) {
int visibleLength = stripAnsi(row[i]).length();
if (visibleLength > colWidths[i]) {
colWidths[i] = visibleLength;
}
}
}
String separator = buildSeparator(colWidths);
System.out.println(separator);
System.out.println(buildRow(headers, colWidths));
System.out.println(separator);
for (String[] row : rows) {
System.out.println(buildRow(row, colWidths));
}
System.out.println(separator);
}
private static String buildSeparator(int[] colWidths) {
StringBuilder sb = new StringBuilder("+");
for (int width : colWidths) {
sb.append("-".repeat(width + 2));
sb.append("+");
}
return sb.toString();
}
private static String buildRow(String[] values, int[] colWidths) {
StringBuilder sb = new StringBuilder("|");
for (int i = 0; i < values.length; i++) {
String value = values[i];
int visibleLength = stripAnsi(value).length();
int padding = colWidths[i] - visibleLength;
sb.append(" ");
sb.append(value);
sb.append(" ".repeat(padding + 1));
sb.append("|");
}
return sb.toString();
}
private static String stripAnsi(String text) {
return text.replaceAll("\u001B\\[[;\\d]*m", "");
}
}
Test both classes together
Both utility classes compile, but you have not seen them in action yet. A quick temporary main method lets you visually confirm that colors render and columns align.
- In TableFormatter.java, add the following temporary main method inside the class, just before the closing }:
// Temporary test method - remove after verifying
public static void main(String[] args) {
// Test colored output from Color class
Color.header("=== Table Formatter Test ===");
Color.success("Success message test");
Color.error("Error message test");
Color.prompt("Prompt test: ");
System.out.println();
System.out.println();
// Test table formatting
String[] headers = {"ID", "Name", "Category", "Price"};
List<String[]> rows = List.of(
new String[]{"1", "Laptop", "Electronics", "999.99"},
new String[]{"2", "Mouse", "Accessories", "29.99"}
);
printTable(headers, rows);
}
What does this test do?
This main method calls each Color helper to confirm colors render in your terminal. It then creates sample data and passes it to printTable() so you can visually verify the table borders and column alignment.
This is a temporary test. You will remove it after confirming everything works.
- Save TableFormatter.java.
- Click the green play icon in the gutter next to the main method, then select Run 'TableFormatter.main()'.
- In the Run tool window at the bottom, you should see colored text (cyan header, green success, red error, yellow prompt) followed by a bordered table like this:
+----+--------+-------------+--------+
| ID | Name | Category | Price |
+----+--------+-------------+--------+
| 1 | Laptop | Electronics | 999.99 |
| 2 | Mouse | Accessories | 29.99 |
+----+--------+-------------+--------+
No colors showing up?
IntelliJ's Run tool window supports ANSI colors by default. If you see raw escape codes like \u001B[32m instead of colors, make sure you are running from IntelliJ's built-in terminal, not an external one that does not support ANSI.
If the table columns look misaligned, double check that you copied the buildRow() method exactly. The padding logic depends on " ".repeat(padding + 1) to add the right number of trailing spaces.
help me troubleshoot why ANSI colors are not showing in IntelliJ
- Now that you have confirmed both classes work, remove the entire main method from TableFormatter.java (everything from public static void main through its closing brace).
- Save TableFormatter.java again. The file should be back to just the four static methods.
Why remove the test method?
Temporary main methods are a quick way to verify isolated classes. You remove them once you are confident the code works, keeping each class focused on a single responsibility. The real main method will live in InventoryApp.java later.
Your two utility classes are complete and tested. Next up, you will create the Item data model and build a repository that reads and writes items to a CSV file.
Create the Item Record and CSV Repository
Your formatting layer is done. The Color class handles colored output and TableFormatter renders aligned tables.
Now you need a way to represent inventory items and persist them between sessions. In this step, you will create an immutable data model using a Java record and build a repository class that reads and writes items to a local CSV file.
In this step, get ready to:
- Define the Item data model as a Java record with CSV serialization methods.
- Build ItemRepository to load, save, and manage items using java.nio.file.
- Test persistence by adding items and verifying the CSV file on disk.
Implement the Item record
Java records give you an immutable data class with automatic equals, hashCode, and toString methods. You declare the fields once in the record header and the compiler generates the rest.
What is a Java record?
A record is a special kind of class introduced in Java 16. You declare components in the header (like constructor parameters), and Java generates private final fields, public accessor methods, and the standard equals/hashCode/toString trio.
Records enforce immutability. Once created, an Item's fields cannot change. To "update" a record, you create a new instance with the modified values.
- In IntelliJ, open src/inventory/Item.java and replace its entire contents with this code:
package inventory;
public record Item(int id, String name, String category, double price, int quantity) {
// Converts this record into a comma-separated string for file storage
public String toCsvLine() {
return id + "," + name + "," + category + "," + price + "," + quantity;
}
// Parses a CSV line back into an Item record
public static Item fromCsvLine(String line) {
String[] parts = line.split(",");
int id = Integer.parseInt(parts[0].trim());
String name = parts[1].trim();
String category = parts[2].trim();
double price = Double.parseDouble(parts[3].trim());
int quantity = Integer.parseInt(parts[4].trim());
return new Item(id, name, category, price, quantity);
}
}
What does this code do?
The record header Item(int id, String name, String category, double price, int quantity) defines five components. Java automatically creates accessor methods like item.name() and item.price().
The toCsvLine() method serializes the record to a string like 1,Laptop,Electronics,999.99,5. The static fromCsvLine() factory method does the reverse, splitting a CSV string and parsing each field back into its correct type.
- Save Item.java.
- Check that IntelliJ shows no red underlines in the file. The project should compile without errors.
Seeing a compile error?
Make sure you replaced the entire file contents, including the class declaration. The keyword record replaces class in the declaration. If IntelliJ does not recognize record, confirm your Project SDK is set to JDK 21 under File > Project Structure.
help me fix a compile error in my Item.java record
Build the ItemRepository class
The repository pattern separates data storage logic from the rest of your application. Your ItemRepository will manage an in-memory list of items and sync it to a CSV file on disk using the modern java.nio.file API.
- Open src/inventory/ItemRepository.java and replace its entire contents with the following file I/O foundation:
package inventory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class ItemRepository {
private final List<Item> items = new ArrayList<>();
private final Path filePath;
// Constructor accepts a filename and immediately loads any existing data
public ItemRepository(String fileName) {
this.filePath = Path.of(fileName);
loadFromFile();
}
// Reads all lines from the CSV file and parses each into an Item
public void loadFromFile() {
items.clear();
if (!Files.exists(filePath)) {
return;
}
try {
List<String> lines = Files.readAllLines(filePath);
for (String line : lines) {
if (!line.isBlank()) {
items.add(Item.fromCsvLine(line));
}
}
} catch (IOException e) {
Color.error("Error reading file: " + e.getMessage());
}
}
// Writes all items back to the CSV file, overwriting previous contents
public void saveToFile() {
List<String> lines = new ArrayList<>();
for (Item item : items) {
lines.add(item.toCsvLine());
}
try {
Files.write(filePath, lines);
} catch (IOException e) {
Color.error("Error writing file: " + e.getMessage());
}
}
}
What does this code do?
- Path.of(fileName) creates a file path object. This is the modern replacement for Paths.get().
- Files.exists(filePath) checks if the CSV file exists before trying to read it. On first run, there is no file yet, so the method returns early.
- Files.readAllLines(filePath) loads every line of the file into a List<String> using UTF-8 encoding.
- Files.write(filePath, lines) overwrites the file with the current items list. It creates the file if it does not exist.
You will not see a visible change yet. The next chunk adds the CRUD operations that complete this class.
- Now add the CRUD methods below the saveToFile() method, inside the class body:
// Returns a defensive copy of the items list
public List<Item> getAll() {
return new ArrayList<>(items);
}
// Finds an item by its ID, returns null if not found
public Item getById(int id) {
for (Item item : items) {
if (item.id() == id) {
return item;
}
}
return null;
}
// Adds a new item and saves immediately
public void add(Item item) {
items.add(item);
saveToFile();
}
// Replaces the item with a matching ID and saves
public void update(Item updatedItem) {
for (int i = 0; i < items.size(); i++) {
if (items.get(i).id() == updatedItem.id()) {
items.set(i, updatedItem);
saveToFile();
return;
}
}
}
// Removes the item with the given ID and saves
public void delete(int id) {
items.removeIf(item -> item.id() == id);
saveToFile();
}
// Calculates the next available ID by finding the current maximum
public int getNextId() {
int maxId = 0;
for (Item item : items) {
if (item.id() > maxId) {
maxId = item.id();
}
}
return maxId + 1;
}
What does this code do?
- getAll() returns a copy of the list so external code cannot accidentally modify the repository's internal state.
- add(), update(), and delete() each call saveToFile() immediately. This means every change is persisted to disk as soon as it happens.
- getNextId() scans for the highest existing ID and returns one higher. This ensures new items always get a unique ID even after deletions.
- Save ItemRepository.java.
- Confirm IntelliJ shows no red underlines. The file should compile cleanly alongside Item.java.
Seeing import errors?
Make sure all six import statements are present at the top of the file. If IntelliJ shows IOException or Files as unresolved, press Alt+Enter on the red symbol and select the java.nio.file or java.io option.
help me resolve import errors in ItemRepository.java
✔️ Awesome, I've got everything!
Great. Double check you have saved both Item.java and ItemRepository.java.
ⓧ I'd like to double check the full code
Here is the complete Item.java:
package inventory;
public record Item(int id, String name, String category, double price, int quantity) {
public String toCsvLine() {
return id + "," + name + "," + category + "," + price + "," + quantity;
}
public static Item fromCsvLine(String line) {
String[] parts = line.split(",");
int id = Integer.parseInt(parts[0].trim());
String name = parts[1].trim();
String category = parts[2].trim();
double price = Double.parseDouble(parts[3].trim());
int quantity = Integer.parseInt(parts[4].trim());
return new Item(id, name, category, price, quantity);
}
}
Here is the complete ItemRepository.java:
package inventory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class ItemRepository {
private final List<Item> items = new ArrayList<>();
private final Path filePath;
public ItemRepository(String fileName) {
this.filePath = Path.of(fileName);
loadFromFile();
}
public void loadFromFile() {
items.clear();
if (!Files.exists(filePath)) {
return;
}
try {
List<String> lines = Files.readAllLines(filePath);
for (String line : lines) {
if (!line.isBlank()) {
items.add(Item.fromCsvLine(line));
}
}
} catch (IOException e) {
Color.error("Error reading file: " + e.getMessage());
}
}
public void saveToFile() {
List<String> lines = new ArrayList<>();
for (Item item : items) {
lines.add(item.toCsvLine());
}
try {
Files.write(filePath, lines);
} catch (IOException e) {
Color.error("Error writing file: " + e.getMessage());
}
}
public List<Item> getAll() {
return new ArrayList<>(items);
}
public Item getById(int id) {
for (Item item : items) {
if (item.id() == id) {
return item;
}
}
return null;
}
public void add(Item item) {
items.add(item);
saveToFile();
}
public void update(Item updatedItem) {
for (int i = 0; i < items.size(); i++) {
if (items.get(i).id() == updatedItem.id()) {
items.set(i, updatedItem);
saveToFile();
return;
}
}
}
public void delete(int id) {
items.removeIf(item -> item.id() == id);
saveToFile();
}
public int getNextId() {
int maxId = 0;
for (Item item : items) {
if (item.id() > maxId) {
maxId = item.id();
}
}
return maxId + 1;
}
}
Test the repository with a temporary main method
Before moving on, you should confirm that items are actually written to disk and can be reloaded. You will add a temporary main method to ItemRepository.java that exercises the add and load cycle.
- Add this temporary main method inside the ItemRepository class, below the getNextId() method:
// Temporary test - delete after verifying
public static void main(String[] args) {
ItemRepository repo = new ItemRepository("items.csv");
repo.add(new Item(repo.getNextId(), "Laptop", "Electronics", 999.99, 5));
repo.add(new Item(repo.getNextId(), "Mouse", "Accessories", 29.99, 50));
System.out.println("Items saved. Reloading from file...");
repo.loadFromFile();
for (Item item : repo.getAll()) {
System.out.println(item);
}
}
What does this test do?
This creates a fresh repository pointing at items.csv, adds two items (which triggers saveToFile() each time), then reloads from disk and prints the results. If both items appear in the output, your file I/O is working correctly.
- Save ItemRepository.java.
- Right-click inside the main method and select Run 'ItemRepository.main()'.
- In the Run panel at the bottom of IntelliJ, you should see output like:
Items saved. Reloading from file...
Item[id=1, name=Laptop, category=Electronics, price=999.99, quantity=5]
Item[id=2, name=Mouse, category=Accessories, price=29.99, quantity=50]
- Check that an items.csv file now exists in your project's root directory (the inventory-manager folder). Open it to confirm it contains two lines of comma-separated data.
Not seeing any output?
If IntelliJ shows a ClassNotFoundException or cannot find the main method, make sure the main method signature is exactly public static void main(String[] args) and that it is inside the ItemRepository class braces.
If you see a file permission error, check that IntelliJ's working directory is set to the project root. Go to Run > Edit Configurations and confirm the Working directory field points to your inventory-manager folder.
help me troubleshoot my ItemRepository test
- Once you have confirmed the test works, delete the entire main method from ItemRepository.java. It was only needed for verification.
- Also delete the items.csv file from your project directory so you start fresh when the full app runs.
- Save ItemRepository.java.
Your data layer is locked in. You have an immutable Item record that serializes to CSV and a repository that handles all file I/O. Next up, you will wire these into an interactive menu so users can create, view, update, and delete items from the terminal.
Build the Interactive Menu and CRUD Operations
You have all the building blocks in place. The Color class prints colored text, TableFormatter renders bordered tables, Item models your data, and ItemRepository handles file persistence.
Now it is time to wire everything together into an interactive application. In this step, you will build the main menu loop and all four CRUD operations so a user can manage inventory items from the terminal.
In this step, get ready to:
- Set up the main loop with an enhanced switch expression.
- Implement view, add, update, and delete operations.
- Handle invalid user input with proper error messages.
Set up the main loop and menu
The entry point of your application needs three things: a way to read user input, access to the repository, and a loop that repeatedly shows the menu until the user exits.
- In IntelliJ, open src/inventory/InventoryApp.java and replace its contents with the following code:
package inventory;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class InventoryApp {
// Read user input from the terminal
private final Scanner scanner = new Scanner(System.in);
// Connect to the CSV-backed repository
private final ItemRepository repository = new ItemRepository("items.csv");
public static void main(String[] args) {
new InventoryApp().run();
}
public void run() {
boolean running = true;
while (running) {
printMenu();
String choice = scanner.nextLine().trim();
// Enhanced switch expression with yield
running = switch (choice) {
case "1" -> { viewAllItems(); yield true; }
case "2" -> { addItem(); yield true; }
case "3" -> { updateItem(); yield true; }
case "4" -> { deleteItem(); yield true; }
case "5" -> {
Color.success("Goodbye!");
yield false;
}
default -> {
Color.error("Invalid option. Please enter 1-5.");
yield true;
}
};
}
scanner.close();
}
private void printMenu() {
System.out.println();
Color.header("========== INVENTORY MANAGER ==========");
System.out.println(" 1. View All Items");
System.out.println(" 2. Add Item");
System.out.println(" 3. Update Item");
System.out.println(" 4. Delete Item");
System.out.println(" 5. Exit");
Color.header("========================================");
Color.prompt("Select an option: ");
}
}
What does this code do?
The run() method uses a while loop that keeps showing the menu until running becomes false.
The enhanced switch expression assigns the result directly to running. Each case calls the appropriate method and uses yield to return true (keep looping) or false (exit). This is more concise than traditional switch-case-break syntax.
What is an enhanced switch expression?
JDK 21 supports switch expressions that return values using arrow labels (->) instead of fall-through case: statements. For multi-line blocks, you wrap the body in braces and use yield to produce the value.
- Save InventoryApp.java.
IntelliJ will show errors for the four missing methods (viewAllItems, addItem, updateItem, deleteItem). You will add them next.
Seeing errors beyond the missing methods?
Make sure your package declaration at the top says package inventory; and that the imports include java.util.Scanner, java.util.ArrayList, and java.util.List. Also confirm that ItemRepository.java and Color.java are in the same inventory package.
Help me debug compile errors in InventoryApp.java
Implement the view and add operations
The first two operations let users see their inventory and add new items. Each method uses the utility classes you built earlier.
- In InventoryApp.java, add the viewAllItems() method below the printMenu() method:
private void viewAllItems() {
List<Item> items = repository.getAll();
if (items.isEmpty()) {
Color.error("No items found. Add some items first!");
return;
}
System.out.println();
Color.header("All Items:");
// Build a row array for each item to pass to the table formatter
String[] headers = {"ID", "Name", "Category", "Price", "Qty"};
List<String[]> rows = new ArrayList<>();
for (Item item : items) {
rows.add(new String[]{
String.valueOf(item.id()),
item.name(),
item.category(),
String.format("%.2f", item.price()),
String.valueOf(item.quantity())
});
}
TableFormatter.printTable(headers, rows);
}
What does this code do?
This method fetches all items from the repository and converts each Item record into a String[] row. It then passes the headers and rows to TableFormatter.printTable() to render the bordered table.
The String.format("%.2f", item.price()) call formats the price to exactly two decimal places, so 999.9 displays as 999.90.
- Now add the addItem() method below viewAllItems():
private void addItem() {
System.out.println();
Color.header("--- Add New Item ---");
// Prompt for name and validate it is not empty
Color.prompt("Name: ");
String name = scanner.nextLine().trim();
if (name.isEmpty()) {
Color.error("Name cannot be empty.");
return;
}
// Prompt for category and validate it is not empty
Color.prompt("Category: ");
String category = scanner.nextLine().trim();
if (category.isEmpty()) {
Color.error("Category cannot be empty.");
return;
}
// Prompt for price with number validation
Color.prompt("Price: ");
double price;
try {
price = Double.parseDouble(scanner.nextLine().trim());
} catch (NumberFormatException e) {
Color.error("Invalid price. Must be a number.");
return;
}
// Prompt for quantity with integer validation
Color.prompt("Quantity: ");
int quantity;
try {
quantity = Integer.parseInt(scanner.nextLine().trim());
} catch (NumberFormatException e) {
Color.error("Invalid quantity. Must be a whole number.");
return;
}
// Create the new Item record and persist it
int id = repository.getNextId();
Item newItem = new Item(id, name, category, price, quantity);
repository.add(newItem);
Color.success("Item added successfully! (ID: " + id + ")");
}
What does this code do?
The addItem() method prompts the user for each field using Color.prompt() (which prints in yellow without a trailing newline). After each input, it validates immediately.
The try-catch blocks around Double.parseDouble() and Integer.parseInt() catch NumberFormatException when a user types text instead of a number. The method returns early with a red error message instead of crashing.
- Save InventoryApp.java.
IntelliJ will still show errors for the two remaining methods. You will add those next.
Red underlines on ArrayList or List?
Make sure you have both import java.util.ArrayList; and import java.util.List; at the top of the file. IntelliJ can auto-import for you. Press Alt+Enter on the red-underlined class name and select the java.util option.
Help me fix import errors in my Java file
Implement update and delete with input validation
The update operation is the most complex. It shows the current values and lets the user press Enter to keep any field unchanged. The delete operation asks for confirmation before removing an item.
- Add the updateItem() method below addItem():
private void updateItem() {
System.out.println();
viewAllItems();
// Prompt for the item ID to update
Color.prompt("Enter ID of item to update: ");
int id;
try {
id = Integer.parseInt(scanner.nextLine().trim());
} catch (NumberFormatException e) {
Color.error("Invalid ID.");
return;
}
Item existing = repository.getById(id);
if (existing == null) {
Color.error("Item with ID " + id + " not found.");
return;
}
// Show current values so the user knows what they are changing
Color.header("Current values (press Enter to keep):");
System.out.println(" Name: " + existing.name());
System.out.println(" Category: " + existing.category());
System.out.println(" Price: " + existing.price());
System.out.println(" Quantity: " + existing.quantity());
// Prompt for new values, keeping existing if input is blank
Color.prompt("New Name [" + existing.name() + "]: ");
String name = scanner.nextLine().trim();
if (name.isEmpty()) name = existing.name();
Color.prompt("New Category [" + existing.category() + "]: ");
String category = scanner.nextLine().trim();
if (category.isEmpty()) category = existing.category();
Color.prompt("New Price [" + existing.price() + "]: ");
String priceInput = scanner.nextLine().trim();
double price;
if (priceInput.isEmpty()) {
price = existing.price();
} else {
try {
price = Double.parseDouble(priceInput);
} catch (NumberFormatException e) {
Color.error("Invalid price. Update cancelled.");
return;
}
}
Color.prompt("New Quantity [" + existing.quantity() + "]: ");
String qtyInput = scanner.nextLine().trim();
int quantity;
if (qtyInput.isEmpty()) {
quantity = existing.quantity();
} else {
try {
quantity = Integer.parseInt(qtyInput);
} catch (NumberFormatException e) {
Color.error("Invalid quantity. Update cancelled.");
return;
}
}
// Create a new Item record with the updated values
Item updatedItem = new Item(id, name, category, price, quantity);
repository.update(updatedItem);
Color.success("Item updated successfully!");
}
What does this code do?
The blank-to-keep pattern works by checking if the user's input is empty. If it is, the method keeps the existing value from the record. This lets users quickly skip fields they do not want to change.
For numeric fields (price and quantity), the method only attempts to parse if the input is non-empty. This avoids a NumberFormatException on blank input while still catching invalid text like "abc".
Since Java records are immutable, you cannot modify the existing Item. Instead, you create a brand new Item with the same ID and the updated field values.
- Add the deleteItem() method below updateItem():
private void deleteItem() {
System.out.println();
viewAllItems();
// Prompt for the item ID to delete
Color.prompt("Enter ID of item to delete: ");
int id;
try {
id = Integer.parseInt(scanner.nextLine().trim());
} catch (NumberFormatException e) {
Color.error("Invalid ID.");
return;
}
Item existing = repository.getById(id);
if (existing == null) {
Color.error("Item with ID " + id + " not found.");
return;
}
// Show the item details and ask for confirmation
System.out.println(" Item: " + existing.name() + " (" + existing.category() + ")");
Color.prompt("Are you sure? (y/n): ");
String confirm = scanner.nextLine().trim().toLowerCase();
if (confirm.equals("y")) {
repository.delete(id);
Color.success("Item deleted successfully!");
} else {
Color.success("Delete cancelled.");
}
}
What does this code do?
The deleteItem() method displays the item table first so the user can see the IDs. After the user enters an ID, it shows which item will be removed and asks for a y/n confirmation before calling repository.delete(id).
The confirmation step prevents accidental deletions. Any input other than "y" cancels the operation.
- Save InventoryApp.java.
- Confirm IntelliJ shows no red underlines. All methods are now defined and the file should compile cleanly.
- Run the application by right-clicking InventoryApp.java in the Project panel and selecting Run 'InventoryApp.main()'.
- You should see a cyan-colored menu in the terminal with options 1 through 5.
- Type 2 and press Enter to test adding an item. Enter Laptop for the name, Electronics for the category, 999.99 for the price, and 5 for the quantity.
- You should see a green success message: Item added successfully! (ID: 1).
- Type 1 and press Enter to view the item in a formatted table.
- Type 5 and press Enter to exit the app.
What about the CSV file?
After adding an item, check the project root in IntelliJ's Project panel. You should see an items.csv file. Open it to confirm your item was saved. If you run the app again, it will reload that CSV and your item will still be there.
- Test input validation by running the app again, selecting 2 to add an item, and typing abc for the price. You should see a red error: Invalid price. Must be a number..
App crashes instead of showing an error?
If you see a NumberFormatException stack trace in the terminal, make sure your try-catch block is wrapping the Double.parseDouble() call. The catch clause should specifically catch NumberFormatException and call Color.error() followed by return; to exit the method gracefully.
Help me debug my input validation in addItem()
✔️ Awesome, I've got everything!
Great. Double check you have saved InventoryApp.java and confirmed the app runs with colored output.
ⓧ I'd like to double check the full code
package inventory;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class InventoryApp {
private final Scanner scanner = new Scanner(System.in);
private final ItemRepository repository = new ItemRepository("items.csv");
public static void main(String[] args) {
new InventoryApp().run();
}
public void run() {
boolean running = true;
while (running) {
printMenu();
String choice = scanner.nextLine().trim();
running = switch (choice) {
case "1" -> { viewAllItems(); yield true; }
case "2" -> { addItem(); yield true; }
case "3" -> { updateItem(); yield true; }
case "4" -> { deleteItem(); yield true; }
case "5" -> {
Color.success("Goodbye!");
yield false;
}
default -> {
Color.error("Invalid option. Please enter 1-5.");
yield true;
}
};
}
scanner.close();
}
private void printMenu() {
System.out.println();
Color.header("========== INVENTORY MANAGER ==========");
System.out.println(" 1. View All Items");
System.out.println(" 2. Add Item");
System.out.println(" 3. Update Item");
System.out.println(" 4. Delete Item");
System.out.println(" 5. Exit");
Color.header("========================================");
Color.prompt("Select an option: ");
}
private void viewAllItems() {
List<Item> items = repository.getAll();
if (items.isEmpty()) {
Color.error("No items found. Add some items first!");
return;
}
System.out.println();
Color.header("All Items:");
String[] headers = {"ID", "Name", "Category", "Price", "Qty"};
List<String[]> rows = new ArrayList<>();
for (Item item : items) {
rows.add(new String[]{
String.valueOf(item.id()),
item.name(),
item.category(),
String.format("%.2f", item.price()),
String.valueOf(item.quantity())
});
}
TableFormatter.printTable(headers, rows);
}
private void addItem() {
System.out.println();
Color.header("--- Add New Item ---");
Color.prompt("Name: ");
String name = scanner.nextLine().trim();
if (name.isEmpty()) {
Color.error("Name cannot be empty.");
return;
}
Color.prompt("Category: ");
String category = scanner.nextLine().trim();
if (category.isEmpty()) {
Color.error("Category cannot be empty.");
return;
}
Color.prompt("Price: ");
double price;
try {
price = Double.parseDouble(scanner.nextLine().trim());
} catch (NumberFormatException e) {
Color.error("Invalid price. Must be a number.");
return;
}
Color.prompt("Quantity: ");
int quantity;
try {
quantity = Integer.parseInt(scanner.nextLine().trim());
} catch (NumberFormatException e) {
Color.error("Invalid quantity. Must be a whole number.");
return;
}
int id = repository.getNextId();
Item newItem = new Item(id, name, category, price, quantity);
repository.add(newItem);
Color.success("Item added successfully! (ID: " + id + ")");
}
private void updateItem() {
System.out.println();
viewAllItems();
Color.prompt("Enter ID of item to update: ");
int id;
try {
id = Integer.parseInt(scanner.nextLine().trim());
} catch (NumberFormatException e) {
Color.error("Invalid ID.");
return;
}
Item existing = repository.getById(id);
if (existing == null) {
Color.error("Item with ID " + id + " not found.");
return;
}
Color.header("Current values (press Enter to keep):");
System.out.println(" Name: " + existing.name());
System.out.println(" Category: " + existing.category());
System.out.println(" Price: " + existing.price());
System.out.println(" Quantity: " + existing.quantity());
Color.prompt("New Name [" + existing.name() + "]: ");
String name = scanner.nextLine().trim();
if (name.isEmpty()) name = existing.name();
Color.prompt("New Category [" + existing.category() + "]: ");
String category = scanner.nextLine().trim();
if (category.isEmpty()) category = existing.category();
Color.prompt("New Price [" + existing.price() + "]: ");
String priceInput = scanner.nextLine().trim();
double price;
if (priceInput.isEmpty()) {
price = existing.price();
} else {
try {
price = Double.parseDouble(priceInput);
} catch (NumberFormatException e) {
Color.error("Invalid price. Update cancelled.");
return;
}
}
Color.prompt("New Quantity [" + existing.quantity() + "]: ");
String qtyInput = scanner.nextLine().trim();
int quantity;
if (qtyInput.isEmpty()) {
quantity = existing.quantity();
} else {
try {
quantity = Integer.parseInt(qtyInput);
} catch (NumberFormatException e) {
Color.error("Invalid quantity. Update cancelled.");
return;
}
}
Item updatedItem = new Item(id, name, category, price, quantity);
repository.update(updatedItem);
Color.success("Item updated successfully!");
}
private void deleteItem() {
System.out.println();
viewAllItems();
Color.prompt("Enter ID of item to delete: ");
int id;
try {
id = Integer.parseInt(scanner.nextLine().trim());
} catch (NumberFormatException e) {
Color.error("Invalid ID.");
return;
}
Item existing = repository.getById(id);
if (existing == null) {
Color.error("Item with ID " + id + " not found.");
return;
}
System.out.println(" Item: " + existing.name() + " (" + existing.category() + ")");
Color.prompt("Are you sure? (y/n): ");
String confirm = scanner.nextLine().trim().toLowerCase();
if (confirm.equals("y")) {
repository.delete(id);
Color.success("Item deleted successfully!");
} else {
Color.success("Delete cancelled.");
}
}
}
Your inventory manager is fully functional. You can create, read, update, and delete items with colored feedback and persistent CSV storage. Next up, the Secret Mission challenges you to add search and filter with highlighted matches.
Secret mission
Add Search and Filter With Highlighted Matches
Your inventory manager handles CRUD operations, but what about finding items quickly? In this mission, you'll add a search feature that filters items by name or category and highlights the matching text in bold yellow right inside the formatted table.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources for future reference or delete them entirely.
Resources you used:
- The inventory-manager project folder containing all Java source files.
- The items.csv data file generated at runtime in the project root.
Keep everything running
No action needed. Choose this if you plan to keep building on this project or want it for future reference.
- Your project takes minimal disk space and has no background processes.
- The items.csv file persists your inventory data between sessions.
- You can reopen the project in IntelliJ IDEA at any time and run it again.
Pause - I'll come back to this later
There are no running processes to stop. This CLI app only runs when you explicitly execute it.
- Close the project in IntelliJ IDEA by clicking File, then Close Project.
- If you want to reset your inventory data without deleting the project, delete only the items.csv file. The app creates a fresh one on next run.
Delete - I don't want to use this again
Remove all project files from your machine.
- Close the project in IntelliJ IDEA by clicking File, then Close Project.
- Delete the entire inventory-manager folder by running this command in your terminal:
rm -rf ~/Desktop/inventory-manager
- Confirm the folder is gone by running:
ls ~/Desktop | grep inventory
You should see no output, confirming the project folder has been removed.
That's a Wrap!
That's a Wrap!
Nice work! You just built a professional command-line inventory manager in pure Java with zero external dependencies.
You've learned how to:
- Built a complete CRUD application with an interactive menu using enhanced switch expressions and Scanner input handling.
- Created a reusable Color utility with ANSI escape codes and a TableFormatter that renders MySQL-style bordered tables with +, -, and | characters.
- Implemented CSV file persistence using java.nio.file.Files and modeled data with Java records for automatic immutability, equals, hashCode, and toString.
- Secret Mission: Added search and filter functionality with highlighted matching text inside table cells.
Ready to quiz yourself?