Home Java Java PrintWriter vs PrintStream Explained — When, Why and How to Use Each

Java PrintWriter vs PrintStream Explained — When, Why and How to Use Each

In Plain English 🔥
Imagine you're a chef writing orders on a notepad. PrintStream is like shouting your order directly to the kitchen — fast, but only in the kitchen's language. PrintWriter is like handing a printed ticket that can be translated into any language the kitchen understands. Both get the order out, but PrintWriter is smarter about handling different languages (character encodings). That's the core difference — and it matters more than most beginners realise.
⚡ Quick Answer
Imagine you're a chef writing orders on a notepad. PrintStream is like shouting your order directly to the kitchen — fast, but only in the kitchen's language. PrintWriter is like handing a printed ticket that can be translated into any language the kitchen understands. Both get the order out, but PrintWriter is smarter about handling different languages (character encodings). That's the core difference — and it matters more than most beginners realise.

Every Java application outputs something — a log line, a report file, a formatted error message, or just a debug print. The tools you reach for in those moments are PrintWriter and PrintStream. They both look similar on the surface, and that's exactly why so many developers use the wrong one — or use the right one the wrong way. Getting this wrong silently corrupts output in international applications and causes bugs that only appear in production with real user data.

Both classes exist to make writing formatted text easier. Without them, you'd be manually converting strings to byte arrays and managing encoding yourself for every write operation. They wrap that complexity behind a clean API and add crucial features like automatic flushing and printf-style formatting. The problem is they were designed for subtly different jobs, and Java's standard library mixes them in ways that confuse even experienced developers — for example, System.out is a PrintStream, not a PrintWriter.

By the end of this article you'll know exactly when to use PrintWriter versus PrintStream, how to avoid the silent encoding bugs that bite international apps, how to wire up auto-flushing correctly, and how to format output like a pro using printf-style methods. You'll also have a clear mental model you can explain in an interview.

What PrintStream Actually Is — and Why System.out Uses It

PrintStream was Java's original 'convenient output' class, introduced in Java 1.0. Its job is to write formatted representations of values — strings, integers, booleans — to an underlying OutputStream. Under the hood it converts characters to bytes using a character encoding, defaults to the platform's default encoding, and then pushes those bytes to the stream.

System.out is a PrintStream. So is System.err. This is a deliberate historical choice — in the early days of Java, everything was byte-oriented and the platform default encoding was considered 'good enough'. That assumption aged badly as Java spread globally.

The defining characteristic of PrintStream is that it never throws a checked IOException. Instead it sets an internal error flag which you can check with the checkError() method. This makes it convenient for casual output (you don't need a try-catch), but dangerous for critical output like file writing — errors can silently disappear. It also operates at the byte level first, meaning character encoding decisions are baked in at construction time and can't easily be changed.

Use PrintStream when you're writing to the console or working with legacy APIs that expect an OutputStream. Don't use it for writing text files in production code.

PrintStreamDemo.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940
import java.io.PrintStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class PrintStreamDemo {

    public static void main(String[] args) throws IOException {

        // System.out is already a PrintStream — this is the one everyone knows
        System.out.println("Logging to the console via System.out (a PrintStream)");

        // You can create a PrintStream that writes to a file
        // Notice we explicitly set UTF-8 to avoid platform-encoding surprises
        try (PrintStream filePrintStream = new PrintStream(
                new FileOutputStream("server_log.txt"),
                true,                          // auto-flush after every println
                StandardCharsets.UTF_8.name()  // always specify encoding!
        )) {
            filePrintStream.println("Server started at: " + java.time.Instant.now());
            filePrintStream.printf("Active threads: %d%n", Thread.activeCount());
            filePrintStream.println("Status: RUNNING");

            // PrintStream swallows IOExceptions — check for silent failures like this
            if (filePrintStream.checkError()) {
                System.err.println("WARNING: PrintStream encountered a write error!");
            }
        }

        // Redirect System.out to a custom PrintStream (useful in testing)
        PrintStream originalOut = System.out; // save the original
        try (PrintStream captureStream = new PrintStream("captured_output.txt")) {
            System.setOut(captureStream);      // redirect all System.out calls
            System.out.println("This line goes to the file, not the console");
        } finally {
            System.setOut(originalOut);        // ALWAYS restore — or you lose your console
            System.out.println("Console output restored successfully");
        }
    }
}
▶ Output
Logging to the console via System.out (a PrintStream)
Console output restored successfully

[server_log.txt contains:]
Server started at: 2024-03-15T10:23:44.812Z
Active threads: 2
Status: RUNNING

[captured_output.txt contains:]
This line goes to the file, not the console
⚠️
Watch Out: Silent ErrorsPrintStream never throws IOException — it silently sets an error flag instead. If you write to a file using PrintStream and the disk is full, your code will happily continue running without crashing. Always call checkError() after critical writes, or better yet, use PrintWriter for file output so real exceptions can surface.

What PrintWriter Does Differently — and Why It's Better for File Output

PrintWriter is the character-oriented successor to PrintStream. It was added in Java 1.1 to fix the encoding mess. Instead of sitting on top of an OutputStream and converting bytes itself, PrintWriter sits on top of a Writer — a character stream that handles encoding cleanly and explicitly.

This distinction is huge in practice. When you wrap a PrintWriter around an OutputStreamWriter with a specific charset, you've got a clean, predictable pipeline: characters go in, they're encoded correctly, bytes come out. No platform-default encoding surprises. No silent encoding mismatches when your app runs on a Windows server with a different default locale than your MacBook.

PrintWriter also doesn't throw IOException from its print/println methods — same as PrintStream — but it adds one critical upgrade: you can construct it from a File or a filename directly and the constructor does throw IOException, so at least the setup phase fails loudly if something's wrong.

The methods you know — print(), println(), printf(), format() — all work exactly the same as in PrintStream. The difference is purely in what's underneath. Use PrintWriter whenever you're writing text to files, network sockets, or any situation where encoding correctness matters.

PrintWriterDemo.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
import java.io.PrintWriter;
import java.io.FileWriter;
import java.io.OutputStreamWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class PrintWriterDemo {

    // Simulated report data — imagine this comes from a database
    record SalesRecord(String region, int unitsSold, double revenue) {}

    public static void main(String[] args) {

        List<SalesRecord> salesData = List.of(
            new SalesRecord("North America", 1420,  98500.00),
            new SalesRecord("Europe",        987,   72340.50),
            new SalesRecord("Asia-Pacific",  2103, 134200.75)
        );

        // Best practice: OutputStreamWriter lets you control encoding explicitly
        // Wrap it in PrintWriter for the convenient print/printf API
        try (PrintWriter reportWriter = new PrintWriter(
                new OutputStreamWriter(
                    new FileOutputStream("quarterly_report.txt"),
                    StandardCharsets.UTF_8  // explicit UTF-8 — never rely on platform default
                )
        )) {
            // Write a formatted report header
            reportWriter.println("=== Q1 Sales Report ===");
            reportWriter.println("Generated: " + java.time.LocalDate.now());
            reportWriter.println();

            // printf works identically to String.format — very readable output
            reportWriter.printf("%-20s %12s %15s%n", "Region", "Units Sold", "Revenue (USD)");
            reportWriter.println("-".repeat(50));

            double totalRevenue = 0;
            for (SalesRecord record : salesData) {
                reportWriter.printf("%-20s %12d %,15.2f%n",
                    record.region(),
                    record.unitsSold(),
                    record.revenue()
                );
                totalRevenue += record.revenue();
            }

            reportWriter.println("-".repeat(50));
            reportWriter.printf("%-20s %12s %,15.2f%n", "TOTAL", "", totalRevenue);

            // PrintWriter also swallows IOExceptions after construction
            // checkError() is your safety net for critical output
            if (reportWriter.checkError()) {
                System.err.println("ERROR: Report write failed — check disk space!");
            } else {
                System.out.println("Report written successfully.");
            }

        } catch (IOException setupException) {
            // This fires if the file can't be created or opened — loud and clear
            System.err.println("Could not create report file: " + setupException.getMessage());
        }
    }
}
▶ Output
Report written successfully.

[quarterly_report.txt contains:]
=== Q1 Sales Report ===
Generated: 2024-03-15

Region Units Sold Revenue (USD)
--------------------------------------------------
North America 1420 98,500.00
Europe 987 72,340.50
Asia-Pacific 2103 134,200.75
--------------------------------------------------
TOTAL 305,041.25
⚠️
Pro Tip: Always Use OutputStreamWriter in the MiddleWhen writing text files with PrintWriter, chain it as: PrintWriter → OutputStreamWriter(charset) → FileOutputStream. This pattern gives you encoding control at the Writer layer, where it belongs. The one-liner new PrintWriter("file.txt") is convenient for quick scripts, but it uses the platform default encoding — which will silently produce corrupted output on servers with a different locale than your dev machine.

Auto-Flushing, Buffering and the Bug That Kills Log Files

Both PrintWriter and PrintStream support auto-flushing, but it works differently in each and understanding this will save you from one of the most frustrating bugs in Java I/O — a log file that's empty when your application crashes.

In PrintStream, auto-flush fires on every write of a byte array, on println() calls, and when a newline character is written. This is fairly aggressive.

In PrintWriter, auto-flush is more conservative. It only fires on println(), printf(), and format() calls — not on plain print() calls. This is actually better behaviour because it avoids the overhead of flushing on every character write, but it surprises people who expect print() to flush.

The deeper issue is buffering. When you chain a PrintWriter over a BufferedWriter (which is a common and correct pattern for performance), data sits in the buffer until it's flushed. If your application crashes before the buffer drains, that data is gone. For log files or audit trails, that's catastrophic.

The rule: for performance-sensitive sequential writes, buffer and flush explicitly at logical boundaries. For log output where completeness matters more than speed, enable auto-flush on the PrintWriter and skip the BufferedWriter layer. Never assume data reached disk just because you called print().

FlushingBehaviourDemo.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.Instant;

public class FlushingBehaviourDemo {

    public static void main(String[] args) throws IOException {

        // ── SCENARIO 1: Auto-flush ENABLED ──────────────────────────────────
        // Pass 'true' as the second argument to enable auto-flush
        // Each println() immediately pushes data through to the file
        // Good for: audit logs, event logs, anything where crash-safety matters
        System.out.println("--- Scenario 1: Auto-flush enabled ---");
        try (PrintWriter auditLog = new PrintWriter(
                new OutputStreamWriter(
                    new FileOutputStream("audit_log.txt"),
                    StandardCharsets.UTF_8
                ),
                true // AUTO-FLUSH ON — every println/printf flushes immediately
        )) {
            auditLog.println("[" + Instant.now() + "] User 'alice' logged in");
            auditLog.println("[" + Instant.now() + "] User 'alice' accessed /admin");
            // Even if the app crashes here, both lines are safely on disk
            auditLog.printf("[%s] Permission denied for user 'alice'%n", Instant.now());
            System.out.println("Audit entries written (each flushed immediately)");
        }

        // ── SCENARIO 2: Buffered writer WITHOUT auto-flush ───────────────────
        // Good for: writing large reports, CSV exports — performance matters
        // Risk: if you forget flush(), data in the buffer is LOST on crash
        System.out.println("\n--- Scenario 2: Buffered, manual flush ---");
        try (PrintWriter reportWriter = new PrintWriter(
                new BufferedWriter(   // BufferedWriter adds a write buffer for speed
                    new OutputStreamWriter(
                        new FileOutputStream("bulk_report.csv"),
                        StandardCharsets.UTF_8
                    ),
                    8192 // 8 KB buffer — reduces filesystem calls significantly
                )
                // No 'true' here — auto-flush is OFF
        )) {
            reportWriter.println("id,name,score");

            for (int studentId = 1; studentId <= 5; studentId++) {
                // print() does NOT auto-flush even with auto-flush enabled on PrintWriter
                // Use println() or printf() if you rely on auto-flush
                reportWriter.printf("%d,Student_%d,%d%n",
                    studentId, studentId, 60 + studentId * 5);
            }

            // The try-with-resources close() calls flush() then close()
            // So data IS written here — but ONLY because we're using try-with-resources
            System.out.println("CSV written — buffer flushed by auto-close");
        }

        // ── SCENARIO 3: The crash simulation ─────────────────────────────────
        // This demonstrates what happens without proper flushing
        System.out.println("\n--- Scenario 3: What happens without flush ---");
        PrintWriter dangerousWriter = new PrintWriter(
            new BufferedWriter(
                new OutputStreamWriter(
                    new FileOutputStream("dangerous_log.txt"),
                    StandardCharsets.UTF_8
                )
            )
        );
        dangerousWriter.println("This line might never reach disk!");
        // Simulating a crash: we 'forget' to close or flush
        // If the JVM exits abnormally here, the file will be empty
        // Always use try-with-resources to prevent this
        dangerousWriter.flush(); // explicitly saving ourselves here
        dangerousWriter.close();
        System.out.println("Saved! But you should use try-with-resources instead.");
    }
}
▶ Output
--- Scenario 1: Auto-flush enabled ---
Audit entries written (each flushed immediately)

--- Scenario 2: Buffered, manual flush ---
CSV written — buffer flushed by auto-close

--- Scenario 3: What happens without flush ---
Saved! But you should use try-with-resources instead.

[audit_log.txt contains 3 timestamped lines]
[bulk_report.csv contains header + 5 student rows]
[dangerous_log.txt contains the line — because we called flush() explicitly]
🔥
Interview Gold: The Auto-Flush DifferenceIn PrintWriter, auto-flush only triggers on println(), printf() and format() — NOT on print(). In PrintStream, auto-flush also triggers when a newline byte is written. This is a favourite interview question. The follow-up is: 'What happens if you wrap PrintWriter in a BufferedWriter and the app crashes?' — The answer is: buffered data is lost unless you flush() or close() cleanly, which is exactly why try-with-resources is non-negotiable for file I/O.

Real-World Pattern — Building a Reusable Report Generator

Let's tie everything together with a pattern you'll actually use at work: a reusable report generator that writes structured output to different destinations — a file for archiving and the console for live monitoring — using the same logic.

This is where PrintWriter really shines. Because both PrintWriter and the Writer that backs System.out share the same abstract Writer interface, you can write one method that outputs to either destination without changing a line of business logic.

The key insight is dependency injection for your writer. Don't hardcode where output goes inside your business logic. Accept a PrintWriter as a parameter. Your caller decides whether that PrintWriter wraps a file, a socket, a string buffer for testing, or System.out. This makes your code dramatically easier to test — pass a StringWriter-backed PrintWriter in unit tests and assert on the captured string instead of reading files from disk.

This pattern also demonstrates why PrintWriter beats PrintStream for library code: PrintWriter wraps Writer, and Writer is the foundation of Java's character I/O hierarchy. You get maximum flexibility with minimal coupling.

ReportGenerator.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.List;

public class ReportGenerator {

    // A simple domain object — imagine this comes from your service layer
    record ProductSummary(String productName, int quantitySold, double unitPrice) {
        double totalRevenue() { return quantitySold * unitPrice; }
    }

    /**
     * Core report logic — completely decoupled from WHERE the output goes.
     * The caller injects the PrintWriter, so this method is trivially testable.
     */
    public static void writeInventoryReport(
            PrintWriter destination,
            List<ProductSummary> products,
            String storeName
    ) {
        destination.println("╔══════════════════════════════════════════╗");
        destination.printf( "║  Inventory Report — %-21s║%n", storeName);
        destination.printf( "║  Date: %-33s║%n", LocalDate.now());
        destination.println("╚══════════════════════════════════════════╝");
        destination.println();

        destination.printf("%-25s %10s %12s %15s%n",
            "Product", "Qty Sold", "Unit Price", "Total Revenue");
        destination.println("─".repeat(65));

        double grandTotal = 0;
        for (ProductSummary product : products) {
            destination.printf("%-25s %10d %,12.2f %,15.2f%n",
                product.productName(),
                product.quantitySold(),
                product.unitPrice(),
                product.totalRevenue()
            );
            grandTotal += product.totalRevenue();
        }

        destination.println("─".repeat(65));
        destination.printf("%-25s %10s %12s %,15.2f%n",
            "GRAND TOTAL", "", "", grandTotal);
        destination.println();
        destination.printf("Report contains %d product line(s).%n", products.size());
    }

    public static void main(String[] args) throws IOException {

        List<ProductSummary> storeInventory = List.of(
            new ProductSummary("Wireless Headphones",  345,  89.99),
            new ProductSummary("USB-C Hub",            892,  34.50),
            new ProductSummary("Mechanical Keyboard",  217, 149.00),
            new ProductSummary("Webcam HD 1080p",      430,  59.95)
        );

        // ── Output 1: Write to a UTF-8 file for archiving ────────────────────
        try (PrintWriter fileDestination = new PrintWriter(
                new OutputStreamWriter(
                    new FileOutputStream("inventory_report_2024_Q1.txt"),
                    StandardCharsets.UTF_8
                ),
                false // buffered — we'll flush on close via try-with-resources
        )) {
            writeInventoryReport(fileDestination, storeInventory, "London Flagship");
            System.out.println("Archived to: inventory_report_2024_Q1.txt");
        }

        // ── Output 2: Same method, same data — now to the console ────────────
        // System.out is a PrintStream but we wrap it as a PrintWriter
        // OutputStreamWriter handles the bridge between Writer and OutputStream
        try (PrintWriter consoleDestination = new PrintWriter(
                new OutputStreamWriter(System.out, StandardCharsets.UTF_8),
                true // auto-flush for console — we want immediate output
        )) {
            System.out.println("\n── Live Console Output ──");
            writeInventoryReport(consoleDestination, storeInventory, "London Flagship");
        }

        // ── Output 3: Testing pattern — capture output in memory ─────────────
        // No files, no console — just a string you can assert against in tests
        StringWriter capturedOutput = new StringWriter();
        try (PrintWriter testDestination = new PrintWriter(capturedOutput)) {
            writeInventoryReport(testDestination, storeInventory, "Test Store");
        }
        String reportAsString = capturedOutput.toString();
        boolean reportIsValid = reportAsString.contains("GRAND TOTAL")
                             && reportAsString.contains("Wireless Headphones");
        System.out.println("\nReport validation passed: " + reportIsValid);
    }
}
▶ Output
Archived to: inventory_report_2024_Q1.txt

── Live Console Output ──
╔══════════════════════════════════════════╗
║ Inventory Report — London Flagship ║
║ Date: 2024-03-15 ║
╚══════════════════════════════════════════╝

Product Qty Sold Unit Price Total Revenue
─────────────────────────────────────────────────────────────────
Wireless Headphones 345 89.99 31,046.55
USB-C Hub 892 34.50 30,774.00
Mechanical Keyboard 217 149.00 32,333.00
Webcam HD 1080p 430 59.95 25,778.50
─────────────────────────────────────────────────────────────────
GRAND TOTAL 119,932.05

Report contains 4 product line(s).

Report validation passed: true
⚠️
Pro Tip: Inject PrintWriter for Testable I/OAccept PrintWriter as a method parameter instead of creating it inside your business logic. In production, pass a file-backed PrintWriter. In unit tests, pass a PrintWriter wrapping a StringWriter and assert on the returned string. You get zero-disk-access unit tests that are fast, reliable and portable — no temp files to create and clean up.
Feature / AspectPrintStreamPrintWriter
Introduced in JavaJava 1.0Java 1.1
Underlying abstractionOutputStream (byte-oriented)Writer (character-oriented)
Encoding controlSpecified at construction, limitedVia OutputStreamWriter — explicit and clean
Throws IOException?Never (uses error flag)Constructor can; print methods never do
Error detectioncheckError() only — silent by defaultcheckError() only — same caveat
Auto-flush triggers onprintln(), byte array write, newline byteprintln(), printf(), format() only
Best use caseConsole output, System.out/errFile output, network streams, reports
Wraps System.out?System.out IS a PrintStreamWrap with new PrintWriter(new OutputStreamWriter(System.out))
Unicode / non-ASCII safetyRisky without explicit encoding argClean when paired with OutputStreamWriter(charset)
Testability patternHard — tied to byte streamsEasy — wrap a StringWriter for in-memory capture

🎯 Key Takeaways

  • PrintStream is byte-oriented and lives at the console layer — System.out and System.err are PrintStreams by design. For file writing in production code, PrintWriter is always the safer, more correct choice.
  • Neither PrintStream nor PrintWriter throws IOException from print/println/printf — they swallow errors silently. Always call checkError() after critical writes, and always use try-with-resources to guarantee flush-then-close on exit.
  • Auto-flush in PrintWriter only fires on println(), printf() and format() — NOT on print(). If you're debugging and your output isn't appearing, that's your most likely culprit.
  • The cleanest file-writing pattern is PrintWriter → OutputStreamWriter(StandardCharsets.UTF_8) → FileOutputStream. This chain gives you formatting convenience, explicit encoding, and a proper character-to-byte conversion pipeline — no platform-default encoding surprises in production.

⚠ Common Mistakes to Avoid

  • Mistake 1: Using new PrintWriter(new FileWriter('output.txt')) — FileWriter uses the platform default encoding. On a developer MacBook (UTF-8) this works fine, but on a Windows server (often Windows-1252) your non-ASCII characters silently corrupt. Fix it: always use new PrintWriter(new OutputStreamWriter(new FileOutputStream('output.txt'), StandardCharsets.UTF_8)) so encoding is explicit and consistent everywhere.
  • Mistake 2: Forgetting that print() doesn't trigger auto-flush in PrintWriter — Developers enable auto-flush on PrintWriter and then call print() expecting immediate output, but nothing appears until the stream is closed or println()/printf() is called. The symptom is missing console output during debugging or partial log files. Fix it: use println() or printf() when you need auto-flush behaviour, or call flush() explicitly after print() calls in time-sensitive output paths.
  • Mistake 3: Not using try-with-resources with file-backed PrintWriter — PrintWriter's print methods swallow IOExceptions silently. If the underlying stream closes unexpectedly and you're not using try-with-resources, your code keeps running, checkError() returns true but nobody checked it, and your output file is truncated or empty. Fix it: always wrap file-backed PrintWriter in try-with-resources — the auto-close calls flush() then close() in order, ensuring buffered data reaches disk even if an exception occurs earlier in the block.

Interview Questions on This Topic

  • QWhat is the fundamental difference between PrintWriter and PrintStream, and which would you use to write a UTF-8 encoded CSV file to disk — and why?
  • QPrintWriter's auto-flush constructor parameter is set to true. A developer calls print() and expects the output to appear immediately on the console, but nothing shows up. What's happening and how do you fix it?
  • QYou have a method that formats and writes a report. How would you design its signature so that it can write to a file in production, to the console during development, and to a String in unit tests — all without changing the method body?

Frequently Asked Questions

Can I use PrintWriter to write to System.out instead of PrintStream?

Yes. Wrap System.out like this: new PrintWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8), true). This gives you a PrintWriter API over the console with explicit encoding and auto-flush on println/printf calls. It's useful when you have a method that accepts a PrintWriter and you want to point it at the console without changing the method signature.

Does PrintWriter automatically flush when it's closed?

Yes — closing a PrintWriter calls flush() first and then close() on the underlying stream. This is why try-with-resources is so important: even if an exception is thrown earlier in the block, the close() in the finally phase will flush your buffered data to disk. Without try-with-resources, a crash before your explicit close() call means buffered data is silently lost.

What's the difference between PrintWriter's print() and println() when auto-flush is enabled?

When auto-flush is enabled on a PrintWriter, println(), printf() and format() trigger a flush after writing — but print() does NOT. This surprises many developers because the PrintStream behaviour is slightly different (it flushes on newline bytes). If your output isn't appearing despite auto-flush being enabled, switch from print() to println() or add an explicit flush() call after your print() statement.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousJava Scanner ClassNext →String Tokenizer in Java
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged