Email isn’t just for newsletters; it’s the backbone of modern application workflows. From sending critical account verification links to delivering daily reports, a reliable email system is non-negotiable. Luckily, Spring Boot makes sending emails clean, configurable, and production-ready with its powerful JavaMailSender interface.
In this comprehensive guide, we’ll walk you through everything you need to know to become an email pro with Spring Boot 3. We’ll cover sending plain text and rich HTML emails, handling attachments, and supercharging your system with asynchronous processing for blazing-fast performance.
Let’s get started.
1. Setting the Stage: Project Setup
First things first, we need to tell our Spring Boot project that we intend to use its mail-sending capabilities. We do this by adding a single dependency to our pom.xml file.
Tony Hoare called null references his “billion-dollar mistake.” In Java, that mistake has a name: NullPointerException. For decades, Java developers guarded against it with cascading if (x != null) checks that cluttered business logic and still failed when someone forgot one. Java 8 introduced java.util.Optional<T> to fix this at the API level: a container type that makes the possibility of an absent value explicit in the method signature, forcing callers to handle it rather than ignore it. This guide is a complete, runnable reference — from creating instances to advanced chaining to the rules every production API should follow.
In 1978, Intel released the 8086 and changed computing permanently. It was the chip that powered the original IBM PC, seeded the x86 architecture that still runs every Windows and Linux machine today, and introduced the pipelined fetch-execute model that every modern CPU descends from. If you are studying microprocessors — for an exam, for embedded systems work, or out of curiosity about how computers actually work at the hardware level — the 8086 is where the story starts. This post covers the processor’s origin and key features first, then moves into the memory segmentation mechanism that underlies every single memory access it ever makes.
Tail recursion is when a function’s recursive call is the last operation before returning, enabling tail-call elimination in languages/runtimes that support it; non-tail recursion performs additional work after the recursive call returns. In Java, tail-call optimization is not guaranteed by the JVM, so tail recursion does not reduce stack usage unless transformed to iterative code; still, converting non-tail recursion to tail style can make iterative conversion straightforward and avoid stack overflow in production.
What is recursion?
Recursion is a divide-and-conquer technique where a function calls itself with smaller inputs until a base case is reached, with the JVM allocating a stack frame for each invocation; deep recursion risks StackOverflowError if not controlled. Each call must eventually reduce the problem and hit a base case to terminate safely and predictably.
In concurrent programming, often you need to schedule work or tasks to happen after a delay: e.g. timeouts, scheduled retries, delayed tasks, rate limiting, delayed processing, etc. Java’s java.util.concurrent package provides a handy queue for such use cases — DelayQueue — which accepts elements that become available only after a specified delay.
In this post, we will explore:
What is a DelayQueue
The Delayed interface
How DelayQueue works internally
Key methods and behaviors
A simple producer/consumer example
Real-world use cases & caveats
Summary
1. What is DelayQueue
DelayQueue<E extends Delayed> is an unbounded blocking queue whose elements must implement the Delayed interface. An element can be retrieved (via take() or poll(...)) only when its delay has expired. Until then, it stays “dormant” inside the queue.
Some important characteristics:
The head of the queue is the delayed element whose expiration (delay) is earliest (i.e. whose delay will expire first).
If no element has expired yet, poll() returns null, and take() blocks until an element becomes available.
The queue is unbounded — operations like put() or offer() never block (unless out of memory).
The queue uses internal locking (ReentrantLock) for thread safety.
Iterators over the queue are weakly consistent and do not guarantee ordered traversal.
In short: DelayQueue is like a priority queue (ordered by expiration time) combined with blocking/waiting semantics.
When developing applications, there are situations where we need to represent and manipulate large collections of binary values – typically true or false. Storing these values in a conventional data structure like a boolean[] or a HashSet<Integer> can be inefficient in terms of memory and speed.
Java provides the BitSet class in the java.util package to address this problem. BitSet represents a sequence of bits that can grow dynamically and provides built-in methods for bit-level manipulation.
Distance Vector Routing (DVR) is a decentralised routing protocol where each router maintains a table containing its estimated shortest distance to every other router in the network. Routers exchange these distance vectors with their neighbours, and each router updates its own table whenever a shorter path is discovered. The algorithm continues iterating until no table changes occur — a state known as convergence. DVR is the conceptual basis for real-world protocols such as RIP (Routing Information Protocol).
This C++ program simulates DVR on a user-defined network. The user specifies the number of nodes and the distances between directly connected node pairs. The program iteratively updates routing tables until convergence and then prints each node’s final routing table showing the shortest distance and the next-hop node to reach every destination.