Skip to main content
Java intermediate Lesson 51 of 58

Java Project Ideas

Five Java project ideas with full descriptions, skills practiced, tech stack recommendations, and difficulty ratings to build your portfolio.

Project 1 — Personal Finance Tracker

Difficulty: Beginner | Estimated time: 2–3 weeks

What You Build

A command-line (and optionally REST) application to track income and expenses. Users can log transactions, categorise them, view summaries by month, and export reports as CSV.

Features

  • Add/remove transactions with amount, category, date, and description
  • View monthly summary: total income, total expenses, net balance
  • Filter transactions by category, date range, or keyword
  • Export filtered results to CSV
  • Budget alerts when spending in a category exceeds a set limit
  • Persist data to a local JSON file (beginner) or SQLite/H2 (intermediate)

Tech Stack

Core Java 17+
├── Jackson           — JSON serialisation/deserialisation
├── JUnit 5 + Mockito — unit testing
├── H2 (optional)     — embedded database
└── PicoCLI           — command-line argument parsing

Sample Domain Model

public record Transaction(
    UUID     id,
    String   description,
    BigDecimal amount,
    Category  category,
    LocalDate date,
    Type      type        // INCOME or EXPENSE
) {}

public enum Category { FOOD, TRANSPORT, UTILITIES, ENTERTAINMENT, SALARY, OTHER }

public class FinanceTracker {
    private final List<Transaction> transactions = new ArrayList<>();

    public void add(Transaction t) { transactions.add(t); }

    public MonthlySummary summarise(YearMonth month) {
        var monthTxns = transactions.stream()
            .filter(t -> YearMonth.from(t.date()).equals(month))
            .toList();

        BigDecimal income  = monthTxns.stream()
            .filter(t -> t.type() == Type.INCOME)
            .map(Transaction::amount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);

        BigDecimal expense = monthTxns.stream()
            .filter(t -> t.type() == Type.EXPENSE)
            .map(Transaction::amount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);

        return new MonthlySummary(month, income, expense);
    }
}

Skills You Practice

  • Records, enums, and sealed interfaces for domain modelling
  • Java Streams for filtering and aggregation
  • File I/O and JSON with Jackson
  • Unit testing with JUnit 5
  • BigDecimal for monetary arithmetic (never use double for money)

Project 2 — URL Shortener Service

Difficulty: Intermediate | Estimated time: 3–4 weeks

What You Build

A REST API (Spring Boot) that shortens long URLs, redirects short codes to the original URL, tracks click statistics, and optionally enforces expiry.

Features

  • POST /shorten — accept a long URL, return a short code (e.g., https://sho.rt/aB3kZ)
  • GET /{code} — redirect to the original URL (HTTP 302)
  • GET /{code}/stats — return click count, creation date, last accessed date
  • Custom alias support — POST /shorten with {"url": "...", "alias": "my-link"}
  • Expiry — short links can have a TTL; expired links return 410 Gone
  • Rate limiting — prevent abuse of the shorten endpoint

Tech Stack

Spring Boot 3
├── Spring Web (REST controllers)
├── Spring Data JPA + PostgreSQL (persistence)
├── Spring Cache + Redis (cache recent redirects)
├── Flyway (database migrations)
├── JUnit 5 + Mockito + TestContainers (testing)
└── Docker Compose (local dev environment)

Sample API

@RestController
@RequestMapping("/api")
public class UrlShortenerController {

    private final UrlShortenerService service;

    @PostMapping("/shorten")
    public ResponseEntity<ShortenResponse> shorten(@Valid @RequestBody ShortenRequest req) {
        ShortUrl shortUrl = service.shorten(req.longUrl(), req.alias(), req.ttlDays());
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(new ShortenResponse(shortUrl.code(), shortUrl.shortUrl()));
    }

    @GetMapping("/{code}")
    public ResponseEntity<Void> redirect(@PathVariable String code) {
        return service.resolve(code)
            .map(url -> ResponseEntity.status(HttpStatus.FOUND)
                .location(URI.create(url.longUrl()))
                .<Void>build())
            .orElse(ResponseEntity.notFound().build());
    }

    @GetMapping("/{code}/stats")
    public ResponseEntity<StatsResponse> stats(@PathVariable String code) {
        return service.getStats(code)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
}

Skills You Practice

  • Spring Boot REST API design
  • Spring Data JPA (entities, repositories, custom queries)
  • Caching with @Cacheable and Redis
  • Database migrations with Flyway
  • Integration testing with TestContainers
  • HTTP redirects, status codes, and caching headers

Project 3 — Real-Time Chat Application

Difficulty: Intermediate–Advanced | Estimated time: 4–6 weeks

What You Build

A WebSocket-based chat application with rooms, user authentication, message history, and real-time delivery. Includes a simple HTML/JS front end.

Features

  • Create and join chat rooms
  • Send and receive messages in real time via WebSocket
  • Message history — last 50 messages loaded on join
  • User presence — show who is online in a room
  • JWT authentication — only authenticated users can connect
  • Persistent message storage in PostgreSQL
  • Read receipts and typing indicators

Tech Stack

Spring Boot 3
├── Spring WebSocket (STOMP over WebSocket)
├── Spring Security (JWT authentication)
├── Spring Data JPA + PostgreSQL
├── Project Loom virtual threads (Java 21)
└── Simple HTML + JS front end (or React if preferred)

Sample WebSocket Config

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic", "/queue");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }
}

@Controller
public class ChatController {

    @MessageMapping("/chat.send/{roomId}")
    @SendTo("/topic/room/{roomId}")
    public ChatMessage sendMessage(@DestinationVariable String roomId,
                                   @Payload SendMessageRequest req,
                                   Principal principal) {
        return messageService.save(roomId, principal.getName(), req.content());
    }
}

Skills You Practice

  • WebSocket programming with STOMP
  • JWT authentication and Spring Security
  • Virtual threads for high concurrency
  • Event-driven architecture
  • Front-end integration (basic JS WebSocket client)

Project 4 — E-Commerce Order Processing System

Difficulty: Advanced | Estimated time: 6–8 weeks

What You Build

A backend system for order processing — product catalogue, shopping cart, order placement, payment simulation, inventory management, and order status tracking. Designed as a set of loosely coupled services communicating via an in-process event bus (or Kafka for the full version).

Features

  • Product catalogue with search and filtering
  • Shopping cart (persisted per session)
  • Order placement with stock validation
  • Payment processing (simulated with configurable success/failure rate)
  • Inventory updates on order confirmation
  • Order status machine: PENDING → PROCESSING → SHIPPED → DELIVERED
  • Admin dashboard endpoint with sales reports
  • Email notifications (simulated via logs or real SMTP)

Tech Stack

Spring Boot 3
├── Spring Data JPA + PostgreSQL
├── Spring Events (or Apache Kafka for advanced version)
├── Spring Scheduler (order status updates)
├── Flyway (schema migrations)
├── Spring Boot Actuator (health, metrics)
├── Micrometer + Prometheus + Grafana (observability)
├── JUnit 5 + Mockito + TestContainers (testing)
└── Docker Compose (full local stack)

Sample Order State Machine

@Service
public class OrderService {

    public Order place(long customerId, List<CartItem> items) {
        validateInventory(items);
        Order order = orderRepo.save(Order.pending(customerId, items));
        eventBus.publish(new OrderPlacedEvent(order.id()));
        return order;
    }

    @EventListener
    public void onPaymentSuccess(PaymentSucceededEvent event) {
        orderRepo.findById(event.orderId())
            .ifPresent(order -> {
                order.transitionTo(OrderStatus.PROCESSING);
                orderRepo.save(order);
                eventBus.publish(new OrderConfirmedEvent(order.id()));
            });
    }

    @EventListener
    public void onPaymentFailure(PaymentFailedEvent event) {
        orderRepo.findById(event.orderId())
            .ifPresent(order -> {
                order.transitionTo(OrderStatus.CANCELLED);
                orderRepo.save(order);
                inventoryService.releaseReservation(order);
            });
    }
}

Skills You Practice

  • Domain-driven design with a rich domain model
  • Event-driven architecture with Spring Events
  • State machine pattern for order lifecycle
  • Transactional boundaries and optimistic locking
  • Observability with Micrometer metrics
  • Complex integration testing with TestContainers

Project 5 — Static Site Generator

Difficulty: Advanced | Estimated time: 5–7 weeks

What You Build

A command-line tool that converts a directory of Markdown files with YAML front matter into a complete HTML website — similar in concept to Hugo or Jekyll, but written in Java. Supports templates, a development server with live reload, and a configurable build pipeline.

Features

  • Parse Markdown files with YAML front matter
  • Apply Handlebars/Freemarker templates
  • Generate a complete HTML site in an output directory
  • Copy static assets (CSS, images, JS)
  • Built-in dev server with live reload on file change
  • Tag and category pages auto-generated
  • Sitemap.xml and RSS feed generation
  • Incremental builds — only regenerate changed files

Tech Stack

Core Java 21
├── CommonMark       — Markdown to HTML parsing
├── SnakeYAML        — YAML front matter parsing
├── Freemarker       — HTML templating
├── WatchService     — file system change detection
├── jdk.httpserver   — built-in lightweight dev server
├── PicoCLI          — CLI argument parsing
└── JUnit 5          — testing

Sample Build Pipeline

public class SiteBuilder {

    public void build(Path sourceDir, Path outputDir) throws IOException {
        var pages = discoverPages(sourceDir);

        // Process in parallel for large sites
        pages.parallelStream().forEach(page -> {
            try {
                var parsed   = markdownParser.parse(page);
                var rendered = templateEngine.render(parsed, globalContext);
                var outPath  = resolveOutputPath(page, outputDir);
                Files.createDirectories(outPath.getParent());
                Files.writeString(outPath, rendered);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });

        copyStaticAssets(sourceDir.resolve("static"), outputDir);
        generateSitemap(pages, outputDir);
        generateRssFeed(pages, outputDir);
    }

    public void serve(Path sourceDir, Path outputDir, int port) throws IOException {
        build(sourceDir, outputDir);

        // File watcher for live reload
        WatchService watcher = FileSystems.getDefault().newWatchService();
        sourceDir.register(watcher,
            StandardWatchEventKinds.ENTRY_CREATE,
            StandardWatchEventKinds.ENTRY_MODIFY,
            StandardWatchEventKinds.ENTRY_DELETE);

        Thread.ofVirtual().start(() -> {
            while (true) {
                WatchKey key = watcher.take();
                // Rebuild changed pages and notify browser via SSE
                rebuildChanged(key.pollEvents(), sourceDir, outputDir);
                key.reset();
            }
        });

        startDevServer(outputDir, port);
        System.out.println("Serving at http://localhost:" + port);
    }
}

Skills You Practice

  • File system APIs (NIO.2, WatchService, Path)
  • Parallel processing with streams and virtual threads
  • Template engines
  • Building a real CLI tool with PicoCLI
  • Parsing structured file formats (Markdown, YAML)
  • Incremental computation and caching

Choosing Your Project

Your GoalRecommended Project
Learn core Java + OOPProject 1 — Finance Tracker
Learn Spring Boot REST APIsProject 2 — URL Shortener
Learn WebSockets + real-timeProject 3 — Chat App
Learn distributed systemsProject 4 — Order System
Learn file processing + CLIProject 5 — Static Site Generator
All of the aboveBuild them in order — each builds on the previous

Frequently Asked Questions

Which project should I start with as a beginner?
Start with the Personal Finance Tracker (Project 1). It uses core Java skills you already have — collections, file I/O, OOP — without requiring a web framework or database setup. Once that is working, add a database and then a REST API layer to grow it naturally.
Do I need Spring Boot for these projects?
Not for all of them. The CLI tools and the finance tracker work fine as standalone Java applications. Spring Boot becomes valuable once you want an HTTP API, dependency injection, or JPA. Projects 2-5 all benefit from Spring Boot, but you can start simpler and add it when you are ready.
How do I make these projects stand out on a resume?
Go beyond the happy path: add proper error handling, logging, input validation, unit tests, and a README with setup instructions. Deploy it somewhere (Railway, Render, or a free-tier cloud VM) so you have a live URL. Reviewers are more impressed by a complete, working project with tests than a half-finished one with many features.
Can I use these projects with a database other than PostgreSQL?
Yes. The projects below suggest PostgreSQL because it is production-quality and free. MySQL works equally well. If you want to skip database setup entirely while learning, H2 (in-memory) runs inside your JVM with no installation — just add it to pom.xml and configure Spring Boot to use it.