Skip to main content

Command Palette

Search for a command to run...

Crashing My Own Java Server (And Why It Made Me a Better Developer)

How revisiting Java fundamentals, sockets, and multithreading taught me lessons no framework could.

Updated
9 min read
Crashing My Own Java Server (And Why It Made Me a Better Developer)
D

I’m a Software Engineer passionate about crafting performant, scalable, and user-centric products. My expertise spans frontend (React, Next.js, React Native) and backend (Node.js, Spring Boot), enabling me to deliver end-to-end solutions.

My focus: turning complex UI/UX into clean, modular systems and integrating them with robust APIs. I believe in “learning in public” — sharing what I build, break, and fix to grow with the developer community.

Introduction

For the longest time, I saw myself as a frontend developer. I loved working with React, Next.js, and React Native — bringing designs to life and making things feel smooth and interactive.

But at some point, I realized something was missing. I didn’t just want to stop at the UI layer. I wanted to peek behind the curtain — to design, build, and scale systems end to end.

Naturally, Spring Boot became my next destination. But before jumping straight into it, I had an important realization:

👉
Was I truly ready for Spring Boot, or I am just going with the vibe?

Frameworks are powerful, but they also abstract away a lot of complexity. Without understanding the building blocks, I’d be “using” Spring Boot without really knowing what’s happening under the hood. And that didn’t sit right with me.

So, I made a plan: before touching Spring Boot, I’d revisit Java OOP, collections, networking, and concurrency.

This article is the story of that detour. Spoiler: it involved trains, tickets, and eventually, crashing my own server (intentionally).


Revisiting Java Fundamentals

This part is fairly easy for me as I already have a decent grip on Java, but I just need to brush up on some essentials:

  1. OOP Principles: encapsulation, abstraction, inheritance, polymorphism.

  2. Collections Framework: ArrayList, HashMap, Set, Optional, and Streams with methods like map(), filter(), and forEach().

  3. Functional Interfaces & Lambdas → because writing boilerplate code is fun… until you’ve done it five times.

But I didn’t want to just read about these concepts. I wanted to use them.

So, I built a Ticket Booking CLI App in Java.

It was simple on the surface — user signup/login, train search, booking/cancellation, and JSON-based persistence.

But behind the scenes, it forced me to:

  1. Design entities and services.

  2. Apply OOP principles to keep things modular.

  3. Work with the Collections API to handle user and booking data.

Although this project is fairly small, but it taught me the essence of structuring backend logic into entities, services and persistence layers.

If you are curious about the code and want to check it out yourself. Here’s the github repo link:

https://github.com/DevanshSK/irctc-ticket-booking-app

This was the first time where working with Java stopped feeling like theory but like a tool for real-world problems.


My Curiosity about Servers

With OOPS and Collections Framework done and dusted, I was one inch closer to the backend developer territory.

💡
What really happens when I type google.com into my browser?

Now, I could’ve just read a blog or watched a YouTube video (there are a million). But where’s the fun in that?
I didn’t want a textbook answer. I wanted to see it, build it, break it.

I knew Spring Boot would eventually abstract away a lot of this stuff — sockets, HTTP handling, thread management — but I didn’t want it to feel like black magic. I wanted to see what was happening under the hood.

That curiosity pushed me to explore networking in Java: sockets, TCP/UDP protocols, client-server communication, and eventually, multithreading.

And that’s where the real fun (and chaos) began: building my own servers.


Step 1: Building a Single-Threaded Server

I started with the most basic setup imaginable: a server socket that listens on a port, accepts a client, and replies with a message.

But instead of hardcoding "Hello from server!", I decided to level it up by reading the response from a text file (response.txt).

That way, the server could return dynamic content without me touching the code.


    private String getResponseMessage() {
        // Get response and 
        StringBuilder response = new StringBuilder();
        try (BufferedReader fileReader = new BufferedReader(new FileReader("response.txt"))) {
            String line;
            while ((line = fileReader.readLine()) != null) {
                response.append(line).append("\n");
            }
        } catch (IOException e) {
            response.append("Error: Could not read response.txt");
            e.printStackTrace();
        }
        return response.toString().trim();
    }

And the server itself:

import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class SingleThreadedServer {

    public static void main(String[] args) throws Exception {
        ServerSocket socket = new ServerSocket(8010);
        System.out.println("Server started. Listening on port 8010...");

        while (true) {
            Socket client = socket.accept(); // blocks until a client connects
            PrintWriter out = new PrintWriter(client.getOutputStream(), true);
            // Send message from response.txt
            String responseMessage = getResponseMessage();
            out.println(responseMessage);
            // Close the resources.
            out.close();
            client.close();
        }
    }

}

And here’s the Client:

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class Client {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("localhost", 8010);
        PrintWriter toServer = new PrintWriter(socket.getOutputStream(), true);
        BufferedReader fromServer = new BufferedReader(new InputStreamReader(socket.getInputStream()));

        toServer.println("Hello from client!");
        String response = fromServer.readLine();
        System.out.println("Server responded: " + response);

        socket.close();
    }
}

Run both of them, and voilà — the client receives whatever message is inside response.txt.

It worked perfectly… until I connected a second client.
Suddenly the server froze like a deer in headlights.

👉
Lesson learned: single-threaded servers can only handle one client at a time.

This was my first aha! moment—if servers worked like this in the real world, the internet takes forever to function.

💻 Full source code for all servers (single-threaded, multi-threaded, and thread pool) is available here: GitHub Repo


Step 2: Evolving it into a MultiThreaded Server

The obvious fix? Threads.
If one client blocks the server, why not spawn a new thread for every incoming connection?

    while (true) {
        Socket clientSocket = serverSocket.accept();
        new Thread(() -> handleClient(clientSocket)).start();
    }

That way, multiple clients can be served in parallel.

import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class MultiThreadedServer {
    public static void main(String[] args) throws Exception {
        int port = 8010;
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("MultiThreaded Server running on port " + port);

        while (true) {
            Socket clientSocket = serverSocket.accept();
            new Thread(() -> handleClient(clientSocket)).start();
        }
    }

    private static void handleClient(Socket clientSocket) {
        try (
            PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))
        ) {
            String request = in.readLine();
            System.out.println("Client says: " + request);
            // Send response from a file
            String responseMessage = getResponseMessage();
            out.println(responseMessage + clientSocket.getInetAddress());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

This instantly felt magical.

This allowed me to connect multiple clients simultaneously. I even wrote a test client that spawned 100 threads to simulate users.

for (int i = 0; i < 100; i++) {
    new Thread(() -> {
        try {
            Socket socket = new Socket("localhost", 8010);
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            out.println("Hello from client " + Thread.currentThread().getName());
            System.out.println(in.readLine());

            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }).start();
}

And it worked! 🎉
100 clients, all talking to the server in parallel.

For a moment, I thought: “This is it. I’ve cracked the secret of backend systems.”
But reality had other plans for me.


Step 3: Stress Testing and Crashes

Curiosity kicked in again: How far could I push this?

So I fired up JMeter and stress-tested my server with:

  • 10k requests

  • 60k requests

  • 600k requests 🚨

The results were brutal:

  • The single-threaded server keeled over instantly.

  • The multi-threaded server survived longer… but eventually my laptop fan screamed like a jet engine, CPU usage hit the roof, and the server crashed.

Why? Because every client spawned a brand-new thread, and threads are not free.
They consume memory, CPU, and context-switching overhead. At some point, the system simply said:

👉
Nope. I’m out.

Step 4: Thread Pools to the Rescue

I researched intensively on how to handle massive amounts of requests. The solution came in the form of thread pools.

Instead of spawning an infinite number of threads, you create a fixed pool of workers. Incoming client requests get queued, and available threads pick them up. This way, resources are managed efficiently.

This sounded familiar as I come from the JavaScript world. And I know exactly what it is exactly about. The Event loop.

I dug deep into it and found some beautiful class named Executor and ExecutorService

In Java, the fix was beautifully simple:

import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class ThreadPoolServer {
    public static void main(String[] args) throws Exception {
        int port = 8010;
        ExecutorService pool = Executors.newFixedThreadPool(100);

        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("ThreadPool Server running on port " + port);

        while (true) {
            Socket clientSocket = serverSocket.accept();
            pool.execute(() -> handleClient(clientSocket));
        }
    }

    private static void handleClient(Socket clientSocket) {
        try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
            // Send response from a file
            String responseMessage = getResponseMessage();
            out.println(responseMessage);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

With this small change, the game completely changed:

  • Requests were handled smoothly.

  • Throughput shot up.

  • No more crashes, even at 600k requests.

For the first time, my server wasn’t just “working” — it was scaling.
And that felt like unlocking a cheat code.


Reflections & Key Takeaways

Looking back at this week of experiments, here’s what I carried forward:

  • OOP + Collections → Not just exam material, but the backbone of backend design.

  • Servers & Multithreading → The raw, messy stuff that frameworks like Spring Boot politely hide from you.

  • Threads vs Thread Pools → The difference between a hobby project and something production-ready.

  • Stress Testing → JMeter doesn’t lie. If your server breaks, it’ll show you exactly where.

But the biggest lesson was this:

👉
Understanding fundamentals first makes frameworks 10x more meaningful.

Now, when I dive into Spring Boot, I won’t just “use” it. I’ll understand what it’s doing — from sockets to thread pools — and why.


Closing Notes

This week wasn’t just about writing servers. It was about laying the foundation for my fullstack journey.

Yes, I crashed my server. Yes, I made my laptop beg for mercy. But in the process, I learned lessons no documentation could teach me.

If you’re on the same path, here’s my advice:

  • Don’t skip the fundamentals.

  • Build tiny projects.

  • Stress test them until they break.

  • And most importantly: learn in public.

Because once you do, frameworks stop feeling like magic tricks — and start feeling like tools you truly command.

Next stop for me: Spring Boot 🚀where these fundamentals will finally meet real-world applications.

Source Code

You can find the complete code I wrote while experimenting with these servers on GitHub:

Here’s the Ticket Booking App repo:

References