Tag Archives: Design Pattern

Core Java – Flow.Processor

Reactive streams address a fundamental problem of modern systems: Producers (sensors, services, user events) deliver data at an unpredictable rate, while consumers (persistence, UI, analytics) can only process data at a limited speed. Without a flow control model , backlogs, storage pressure, and ultimately outages occur.

With Java, java.util.concurrent.Flow provides a minimalist API that standardises this problem: Publisher → Processor → Subscriber, including backpressure. Flow.Processor<I,O> is the hinge between upstream and downstream: process, transform, buffer, throttle, and at the same time correctly respect the demand.

Abstract: Reactive Streams = asynchronous push model with backpressure. Java Streams = synchronous PullModel without backpressure.

1.1 Why Reactive Streams?

  • Controlled load (backpressure): Consumers actively signal how many items they can process (request(s)) by requesting the exact number of items that they can currently process safely via their subscription. The publisher may only deliver as many items as previously requested, maintaining a controlled processing speed, ensuring buffers do not overflow, and keeping memory consumption predictable. The demand can be adjusted dynamically (e.g. batch request(32) or item-wise request(1)). In case of overload, the consumer can pause or cancel cleanly by calling cancel().
  • Asynchrony & Decoupling: Data flows can cross thread and service boundaries. This means that a publisher is operated in its own thread or via an executor, for example, while the subscriber processes in a different context. In between, there can be queues or network boundaries, for example, when events are sent from one microservice to another. Loose coupling allows systems to scale and distribute without processing being tightly tied to a single execution context.
  • Infinite/long flows: Telemetry, logs, and EventStreams are often continuous, potentially infinite data sources. A single pull, as is standard in classic Java streams, is insufficient here because the data is not available at a fixed time, but rather constantly accumulates. Instead, you need a permanent subscription that continuously informs the consumer about new events. This is the only way to process continuous data streams without the consumer having to poll or repeatedly start new streams actively.
  • Error semantics & completion: Consistent signals such as onError and onComplete ensure that each data pipeline receives a well-defined closure or transparent error handling. Instead of unpredictable exceptions that occur somewhere in the code, there are standardised callback methods that clearly mark the lifetime of a stream. This enables the reliable distinction between a data stream that has been terminated regularly and one that has encountered an error, allowing downstream components to react accordingly, such as through cleanup, retry mechanisms, or logging. This predictability is crucial for robust pipelines that are to run stably over more extended periods of time.
  • Composability: Composability plays a significant role: A processor can be used as a transformation, for example, by modifying incoming data according to specific rules – similar to a map or filter operation in Java streams. In addition, control operators can be implemented, such as a throttle that limits the rate or a debounce, which only passes on the last signal of a short period of time. Finally, a processor also enables the construction of more complex topologies, such as FanIn, where several sources are merged, or FanOut, where a data stream is distributed among multiple subscribers. This allows entire processing pipelines to be assembled in a modular manner without the individual components having to know their internal logic.
  • Operation & Security: Backpressure is not only used for load control, but also acts as effective protection against denial‑of service attacks. A system that writes data to infinite buffers without restraint can easily be paralysed by excessive input. With correctly implemented backpressure, on the other hand, every demand is strictly enforced: The publisher only delivers as many elements as were actually requested. This prevents malicious or faulty sources from overloading the system by producing data en masse. Instead, resource consumption remains predictable and controlled, increasing the stability and security of the entire pipeline.

Where does that fit in the JDK?

  • Namespace: java.util.concurrent.Flow – this package bundles the core building blocks of the reactive API. It provides interfaces for publishers, subscribers, subscriptions, and processors, and thus forms the standard framework for reactive streams in the JDK. This ensures that all implementations adhere to the same contracts and can be seamlessly integrated.
  • Roles: The roles can be distinguished as follows: A publisher creates and delivers data to its subscribers, a subscriber consumes this data and processes it further, the subscription forms the contract between the two and regulates the backpressure signals, among other things, and the processor takes on a dual role by receiving data as a subscriber and at the same time passing on transformed or filtered data to the next consumer as a publisher.
  • The types in the Flow API are generic and require careful and thread-safe handling. Generic means that Publisher, Subscriber, and Processor are always instantiated with specific type variables, so that the data type remains consistent throughout the pipeline. At the same time, strict attention must be paid to thread safety during implementation, as signals such as onNext or onError can arrive from different threads and must be synchronised correctly. As an introduction, the JDK provides a reference implementation with the SubmissionPublisher, which is sufficient for simple scenarios and already takes into account essential concepts such as backpressure. In production systems, however, developers often fall back on their own or adapted processor classes to precisely implement specific requirements for buffering, transformation, error handling or performance.

1.2 Push vs. Pull: Reactive Streams vs. Java Streams

AspectJava Streams (Pull)Reactive Streams / Flow (Push)
Data directionConsumer pulls dataProducer pushes data
Executiontype. synchronously in the calling threadasynchronous, often via executor/threads
Backpressurenon-existentcentral concept (request(s))
Lifetimefinite, terminal operationPotentially infinite, subscription-based
ErrorExceptions in the calleronError‑signal in the stream
DemolitionEnd of Sourcecancel() of the subscription

Minimal examples

Pull (Java Streams) – the consumer sets the pace:

Note: SubmissionPublisher is a convenient way to get started, but not a panacea (limited tuning options). Custom processor‑implementations give you full control over backpressure, buffer, and concurrency.

1.3 Typical Use Cases

  • Telemetry & Logging: In modern systems, events are continuously created, such as metrics or log entries. These streams can be so high frequency that they cannot be permanently stored or transmitted in their raw form. This is where a processor comes into play, which first caches the events and then combines them in batches, for example, 100 events each. In this way, the flood of data can be effectively throttled and persisted in manageable portions, preventing overloading of individual components.
  • Message ingestion: In many architectures, message and event systems, such as Kafka, classic message queues, or HTTP endpoints, form the input layer for data. These can be modelled as publishers who continuously provide new events. A processor then takes over the validation and possible enrichment, such as checking mandatory fields, adding additional metadata or transforming them into a uniform format. Only then does the enriched and checked data pass on to the subscriber, who takes over the actual persistence, for example, by writing it to a database or data warehouse. In this way, a clear separation of responsibility is achieved, allowing errors to be detected and addressed early in the data stream.
  • IoT/streams from sensors: In practice, sensors often deliver data streams at highly fluctuating speeds. Some sensors transmit data in millisecond intervals, while others transmit data only every few seconds or minutes. If these flows are passed on unchecked to a central consumer, there is a risk of buffer overflow and thus instability in the overall system. With backpressure, however, the consumer can actively control how many measured values they take. In scenarios where the rate exceeds the processing capacity, additional strategies can be employed: either older values are discarded (DropStrategy) or only the most recent value is passed on (LatestStrategy). This keeps the system stable, allowing the consumer to always work with up-to-date and relevant data without being overwhelmed by data avalanches.
  • UIEvents & RateControl: In user interfaces, event streams with very high frequencies quickly arise, for example, when a user types a search query and an event is triggered for every keystroke. Without regulation, these events would be passed on to the BackendService unhindered, which could overload both the network connections and the servers. In this case, a special DebounceProcessor ensures that only the last event is forwarded within a short period of time. This avoids unnecessary inquiries, and the user still receives up-to-date suggestions promptly. This technique effectively prevents the UI from being flooded with data and, at the same time improves the perceived performance and responsiveness of the application.
  • Security: For safety-critical applications, it is imperative that each stage of a data pipeline is clearly delineated and only communicates via defined contracts. This prevents the creation of an unbridled shared state that could lead to inconsistencies or security gaps. Another aspect is the possibility of interrupting data flows cleanly at any time. A cancel() method allows a subscriber to terminate the subscription if they determine that the source is faulty, provides too much data, or presents a potential security threat. In this way, resources can be freed up immediately, and the pipeline remains stable and resilient even under adverse conditions.

2. Overview: java.util.concurrent.Flow

After explaining the motivation and the basic differentiation from Java Streams in the first chapter, we will now deal with the actual framework that the JDK provides for reactive streams. With the java.util.concurrent.The Flow package, a small but powerful API, has been provided since Java 9, describing the essential roles and contracts of Reactive Streams. These interfaces are deliberately kept minimal, allowing them to be used directly for simple use cases as well as forming the basis for more complex libraries.

The focus is on four roles that define the entire lifecycle of publishers and subscribers: Publisher, Subscriber, Subscription and Processor.

A publisher is the source of the data. He produces elements and makes them accessible to his subscribers. He may only send data if it has been explicitly requested beforehand. This prevents him from sending events uncontrollably, which in the worst case, fizzle out into nowhere or cause memory problems.

A subscriber is the counterpart to this: they receive the data that the publisher provides and process it further. To avoid being overloaded, the subscriber informs the publisher of the number of items they can include at a given time as part of the subscription.

The subscription is the link between the publisher and the subscriber. It is created at the moment of registration (the so-called subscribe process) and handed over to the subscriber. From then on, it controls the flow of data by providing methods such as request(long n) and cancel(). This controls demand and, simultaneously, enables a clean interruption of data transmission.

Finally, a processor combines both roles: it is a subscriber itself because it receives data from a publisher, but at the same time, it is also a publisher because it passes on the transformed or filtered data. This dual function is the great strength of the processor: it enables the construction of complex pipelines from simple building blocks, where transformation, filtering, or aggregation are clearly separated and encapsulated.

2.1 Life cycle of the signals

The flow within the Flow API follows a clear order. As soon as a subscriber logs in to a publisher, the publisher first calls the subscriber’s onSubscribe method and passes the subscription. Only when the subscriber requests elements via this subscription – such as request(1) or request(10) – does the publisher begin to deliver data. Each delivered item is delivered by a call to onNext.

If all data has been sent or the stream is terminated for other reasons, the publisher signals the end with onComplete. If, on the other hand, an error occurs, it is reported via onError . This precise signal semantics ensures that every data flow is either completed regularly or terminated with an error message. A subscriber, therefore, always knows exactly what state the data source is in and can react accordingly.

2.2 Backpressure basic principle

A central concept of Flow is the so-called backpressure. It prevents publishers from flooding their subscribers with data. Instead, the subscribers themselves control how many items they want to retrieve. For example, a subscriber can initially request only a single element with request(1) and only ask for the next one after it has been processed. Similarly, it is possible to order larger quantities, such as request(50), to increase throughput. This model gives consumers complete control and ensures a stable balance between production and consumption.

Backpressure is not only a technical detail, but a decisive criterion for the robustness of reactive systems. Without this control, publishers could deliver events unchecked, overloading resources such as CPU or memory. Backpressure, on the other hand, can also be used to implement complex scenarios such as batching, prioritisation or flow control across thread and system boundaries.

The Flow API in the JDK defines the elementary building blocks of a reactive pipeline. Publisher, subscriber, subscription and processor together form a clear model that supports both small experiments and upscaled systems. The signal lifecycle and backpressure mechanisms ensure that the data streams are controlled, traceable and stable. This lays the foundation for delving deeper into the role of the processor in the following chapters and shedding light on its concrete applications.

3. Focus on Flow.Processor<I,O>

After the four roles were briefly described in the previous chapter, the focus now shifts to the processor. It occupies a key position in the flow API because it fulfils two roles simultaneously: it is a subscriber to its upstream, i.e., it receives data from a publisher, and it is also a publisher to its downstream, i.e., it passes on transformed or filtered data to other subscribers. This makes it act like a hinge in a chain of processing steps.

A processor is always provided with two type parameters: <I, O>. The first type describes which elements it receives from the upstream, and the second type describes which elements it passes on to the downstream. This allows any transformations to be mapped – for example, from raw sensor data to validated measured values or from text messages to structured objects.

3.1 Contract & Responsibilities

Implementing a processor means that you must abide by the rules of both subscribers and publishers. This includes, in particular, the following aspects:

  • Pay attention to signal flow: The processor must handle all signals it receives from the upstream device wholly and correctly. These include the onSubscribe, onNext, onError,  and onComplete methods. Each of these signals fulfils a particular function in the lifecycle of a data stream: onSubscribe initiates the subscription and hands over control of the data flow, onNext signals the actual data, onError signals the occurrence of an error, and onComplete marks the regular end of the stream. It is essential that the processor strictly adheres to these semantics: An error may only be reported once and unambiguously, and no further data may follow after the onComplete event occurs. Only through these clear rules does processing remain consistent, predictable and reliable for all components involved.
  • Respect backpressure: As a subscriber, the processor is obligated to adhere strictly to the rules of the backpressure model. This means that it may only receive and process exactly as many elements as it has previously actively requested from the upstream via its subscription – for example, by making a call such as request(1) or request(10). This prevents him from being overrun by a publisher who is too fast. At the same time, as a publisher, he has the responsibility to pass on this logic unchanged to his downstream. He is therefore not allowed to forward more elements to his subscribers than they have explicitly requested. The processor thus acts as a pass-through for the demand signals, ensuring that the entire chain, from publisher to processor to subscriber, remains in balance and that there are no overloads or data losses.
  • Manage resources cleanly: A processor must also ensure that the resources it manages can be released cleanly at all times. In particular, this includes the fact that subscriptions can be actively terminated, for example, by calling cancel() if a subscriber loses interest or processing is to be interrupted for other reasons. It is equally vital that there are no memory or thread leaks: Open queues must be emptied or closed, background threads must be properly terminated, and executor services must be shut down again. Only through this consistent management can the system remain stable, performant and free of creeping resource damage in the long term.
  • Do not swallow errors: If an error occurs, it must be consistently and transparently passed on to the downstream system via the onError method. This ensures that downstream subscribers are also clearly informed about the status of the data stream and can react – for example, through logging, retry mechanisms or the targeted termination of their own processing. If, on the other hand, an error is tacitly ignored, there is a risk of inconsistencies that are difficult to understand. Equally problematic is the uncontrolled throwing of unchecked exceptions, as they are not caught cleanly in the context of reactive pipelines; thus, entire processing chains can enter an undefined state. Clean error propagation is therefore a central quality feature of every processor implementation.

Type Variables & Invariants

The type parameters can be used to determine precisely what kind of data a processor can process. For example, a processor<string, integer> could receive lines of text and extract numbers from them, which it then passes on. These types must be strictly adhered to a subscriber who expects integers must not receive strings unexpectedly. The genericity in the Flow API ensures that errors of this kind are already noticeable during compilation.

Additionally, the invariant holds that a processor must behave like both a correct subscriber and a correct publisher. He is therefore not only responsible for the transformation, but also for the proper transfer of tax information, such as demand and cancellation.

Chain position: between upstream & downstream

In practice, a processor is rarely used in isolation, but as an intermediate link in a chain. An upstream publisher delivers data that the processor receives, transforms or filters and then forwards to downstream subscribers. This creates flexible pipelines that can be expanded or exchanged depending on requirements.

For example, a logging processor can be placed between a data source and the actual consumer, also to record all elements. A processor can also be used to buffer data before it is passed on in larger batches. The position in the middle allows different aspects, such as transformation, control and observation, to be separated from each other and made modular.

The Flow.Processor is the link in reactive pipelines. Through his dual function as subscriber and publisher, he takes responsibility for the correct processing of signals, the implementation of transformations and compliance with the backpressure rules. Its type parameters <I,O> also ensure that data flows remain strictly typed and thus reliable. In the following chapters, we will show how to implement your own processor classes and which patterns have proven themselves in practice.

4. Practical introduction with the JDK

Now that the concepts and contracts around the Flow.Processor have been presented in detail, it is time to start with a practical implementation in the JDK. Fortunately, Java has provided a reference implementation for a publisher since version 9, with the SubmissionPublisher, which is ideal for initial experiments. It relieves you of many details, such as thread management and internal buffering, allowing you to concentrate fully on the data flow.

The SubmissionPublisher is designed to distribute data asynchronously to its subscribers. In the background, it works with an executor that packages and distributes the individual signals, such as onNext, onError and onComplete, into separate tasks. By default, it uses the ForkJoinPool.commonPool(), but it can also pass its own executor. For small experiments, the standard configuration is usually sufficient; however, in production scenarios, it is worthwhile to adapt the executor and buffer sizes to your specific requirements.

4.1 SubmissionPublisher in a nutshell

The SubmissionPublisher<T> class implements the Publisher<T> interface and is therefore an immediately usable source of data. It provides the submit(T item) method, which is used to add new items to the stream. These items are then automatically distributed to all registered subscribers. In this way, the publisher assumes the role of an asynchronous dispatcher, passing incoming data to any number of subscribers.

An essential detail is that the submit method does not block, but buffers the elements internally and then distributes them. However, if the buffer limits are reached, submit can block or even throw an IllegalStateException if the system is under too much load. That’s why it’s crucial to find the right balance between publisher speed, buffer size, and subscriber demand.

4.2 Simple pipeline without its own processor

To understand how SubmissionPublisher works, it’s worth taking a simple example. This creates a publisher that sends numbers from 1 to 5 to a subscriber. The subscriber receives the data and outputs it to the console. This example shows the basic flow of onSubscribe, onNext, and onComplete.

In this program, the publisher asynchronously generates five values, which are transmitted to the subscriber one after the other. The subscriber explicitly requests an additional element each time, creating a controlled and stable flow of data. Finally, the publisher signals with onComplete that no more data will follow.

4.3 Limitations of SubmissionPublisher

Even though the SubmissionPublisher is very helpful for getting started, it quickly reaches its limits in more complex scenarios. For example, it offers only limited possibilities for configuring the backpressure behaviour. While the buffer size is customizable, it doesn’t directly support more complex strategies, such as dropping or latest-value. Additionally, the standard executor is not suitable for all applications, particularly when high latency requirements or strict thread isolation are necessary.

Another point is that SubmissionPublisher is primarily intended for learning purposes and simple applications. In productive systems, people usually rely on their own publisher and processor implementations or on established reactive frameworks such as Project Reactor or RxJava, which are based on the Flow API and provide additional operators.

The SubmissionPublisher is a handy place to start to see the concepts of the Flow API in action. It shows how publishers and subscribers interact, how backpressure works and how signals are processed. At the same time, however, it also becomes clear that for more complex or productive scenarios, in-house processor implementations or external libraries are indispensable. In this way, he forms the bridge between the theoretical foundations and the first practical steps in working with reactive pipelines in the JDK.

5. Sample Processor with Code Examples

Now that the foundations for your own implementation of a processor have been laid, it makes sense to look at typical patterns. They show how concrete use cases can be implemented and form the basis for more complex pipelines. Here are some simple but commonly used processor types, each with sample code.

5.1 MapProcessor<I,O> Transformation

The MapProcessor is the simplest and at the same time one of the most useful processors. It takes on the role of a transformation: incoming data is changed with a function and then passed on. For example, strings can be converted to their length or numbers can be squared.

This allows data streams to be elegantly transformed without adjusting the rest of the pipeline.

5.2 FilterProcessor<T> – Filtering Data

Another typical pattern is filtering. Only those elements that meet a certain condition are passed on. This is especially helpful when large amounts of data are generated, of which only a fraction is relevant.

With this processor, data streams can be reduced in a targeted manner and made more relevant.

5.3 BatchingProcessor<T> – Collect and Share

Sometimes it doesn’t make sense to pass on every single element right away. Instead, they want to collect data and pass it on in blocks. The BatchingProcessor does exactly this job.

This processor is useful for processing data efficiently in blocks, for example when writing to a database.

5.4 ThrottleProcessor<T> Throttling

Especially with high-frequency sources, it is necessary to limit the rate of data passed on. A ThrottleProcessor can be implemented in such a way that it only forwards one element at certain time intervals and discards the rest.

This pattern can be used, for example, to prevent a UI event stream from generating too many updates uncontrollably.

The sample processors presented here are fundamental building blocks for designing reactive pipelines in the JDK. With transformation, filtering, batching and throttling, many typical requirements are covered. The following chapters will focus on implementing backpressure in practice and discuss which strategies have proven successful in operation.

6. Backpressure in practice

After presenting concrete sample processors in the previous chapter, we will now focus on one of the central topics in the Flow API: Backpressure. While the theory sounds simple – the subscriber determines the number of elements they can process – in practice, it turns out that the right implementation is crucial for stability, efficiency and robustness.

6.1 Strategies for dealing with backpressure

Backpressure prevents a publisher from flooding its subscribers with data. Nevertheless, there are different strategies for coping with demand:

  • Bounded buffering: In this strategy, the publisher only keeps a clearly defined, limited number of elements in an internal buffer. Once this buffer is filled, there are several possible reactions: the submit() method  can block until there is space again, or it throws an exception to indicate the overload. In this way, an uncontrolled growth of the memory is prevented. This model is particularly suitable in scenarios where a controlled and predictable data rate is required – for example, in systems that are only allowed to process a certain number of requests per second or in hardware interfaces with limited processing capacity.
  • Dropping: When overloaded, new elements are deliberately discarded instead of being cached in an infinite buffer. This strategy makes sense, especially where not every single event is critical and the loss of individual data points remains tolerable. Typical examples are telemetry data, which is generated at a high frequency anyway, or UI events such as mouse movements or keystrokes, where only the current state is relevant for further processing. Dropping keeps the system responsive and resource-efficient even under extreme load, as it doesn’t try to funnel every single event through the pipeline at all costs.
  • Latest-Value: In the Latest-Value strategy, old elements are not collected in a buffer, but are consistently discarded as soon as a new value arrives. Only the most recently delivered element is stored in each case, so that the subscriber only receives the latest version the next time it is retrieved or asked. This procedure is beneficial when values change continuously. Still, only the last state is relevant for processing – for example, in cases where sensor data, such as temperature or position coordinates, is involved. This relieves the subscriber because he does not have to work through all the intermediate results, but can continue working directly with the latest information.
  • Blocking: In the blocking strategy, the publisher actively waits for demand to be signalled again by the subscriber before delivering further elements. In practice, this means that the calling thread blocks until a corresponding request is made. This approach is comparatively easy to implement and makes the process calculable at first glance, as it ensures that no more data is produced than was requested. However, this approach has a significant drawback: blocked threads are a scarce resource in highly concurrent systems, and when many publishers block at the same time, entire thread pools can become clogged. Therefore, blocking should only be used in very special scenarios, such as when the data rate is low anyway or when controlling the exact data flow is more important than maximum parallelism.

6.2 Demand Policy: Batch vs. Single Retrieval

Subscribers can determine for themselves how many items they call up at a time. There are two common approaches:

  • Single retrieval (request(1)): After each item received, exactly one more is requested. This approach is simple and provides maximum control, but it generates a large number of signals and can become inefficient at very high rates.
  • Batch retrieval (request(n)): The subscriber requests several elements at once, such as request(32). This reduces the signal load, allowing the publisher to deliver items in blocks more efficiently. However, the subscriber must then make sure that he can handle these batches.

In practice, both approaches are often combined, depending on whether low latency or high throughput is the primary concern.

6.3 Monitoring, Tuning and Capacity Planning

A decisive success factor in the use of backpressure is monitoring. Without measuring points, it remains unclear whether a system is running stably or is already working at its limits. The following key figures are helpful:

  • Queue depth: The queue depth refers to the number of elements currently in the internal buffer that the subscriber has not yet retrieved. It is a direct indicator of whether the publisher is rushing away from the consumer or whether demand is in balance with production.
  • Latency: Latency refers to the time elapsed between the creation of an element and its final processing by the subscriber. It is a key measure of the system’s responsiveness: high latency indicates that elements are jamming in buffers or processing steps are taking too long. In contrast, low latency means a smooth data flow.
  • Throughput (items per second): Throughput indicates the number of items that can be processed per unit of time. It’s a measure of the entire pipeline’s performance: high throughput means that publishers, processors, and subscribers can work together efficiently and meet demand. If the throughput drops below the expected level, this is an indication of a bottleneck – whether due to too small buffers, too slow processing in the subscriber, or insufficient parallelisation. Throughput is therefore an important key figure for realistically estimating the capacity of a system and making targeted optimisations.

This data can be used to identify bottlenecks and take appropriate measures, such as adjusting the batch size, increasing the buffers, or introducing additional processor stages.

Concurrency & Execution Context

A central feature of the Flow API is its close integration with concurrent processing. Unlike classic Java streams, which usually run synchronously in the same thread, reactive streams almost always involve an asynchronous interaction of several threads. Therefore, it is crucial to understand the role of the execution context in detail.

7.1 Executor Strategies

The Flow API itself does not specify on which threads the signals are processed. Instead, the implementations determine which executor they use. The SubmissionPublisher uses the ForkJoinPool.commonPool() by default, but also allows you to include your own executor. This is extremely important in practice, as the choice of the executor can change the behaviour of the entire system:

  • A CachedThreadPool can enable very high parallelism through its dynamic generation and reuse of threads. It grows indefinitely when there are many tasks at the same time, and reduces again when there is less work. This makes it exceptionally flexible and well-suited for unpredictable load peaks. However, this dynamic can also lead to uncontrollable behaviour: If thousands of threads are suddenly created, the context switching load increases significantly, which has an adverse effect on latency. It is therefore less suitable for latency-critical scenarios, as response times can become unpredictable due to the administrative overhead and the potentially high number of threads.
  • A FixedThreadPool works with a fixed number of threads, which are defined when the pool is created. In this way, it creates predictable limits for concurrency and prevents uncontrolled growth of the thread count. This makes it particularly suitable for scenarios in which resources such as CPU cores or memory are to be used in a clearly limited and predictable manner. However, there is a disadvantage when overloaded: If there are more tasks than threads available, queues form. These lead to increasing latencies and can become problematic in critical systems if the queue grows unchecked. It is therefore essential to consciously dimension the pool and possibly use mechanisms such as backpressure or rejection policies to absorb overload situations in a controlled manner.
  • The ForkJoinPool is optimised for fine-grained, recursive, and highly parallelizable tasks. He relies on a work-stealing procedure in which worker threads actively search for functions that are in the queues of other threads when they themselves have no more work. This achieves a high utilisation of the available threads and makes efficient use of computing time. This model excels above all in CPU-intensive calculations, which can be broken down into many small, independent tasks – for example, in divide-and-conquer algorithms or parallelised data analyses. The ForkJoinPool, on the other hand, is less suitable for blocking operations such as network access, file system or database queries. When a worker thread blocks, it becomes unavailable for work stealing, making the pool scarce, and the model’s efficiency suffers. In such cases, it is advisable to outsource blocking work to separate executors with a suitable thread policy or to rely on the virtual threads introduced by Loom, which are more tolerant of blocking operations. Additionally, in specific scenarios, the use of ForkJoinPool is applicable. ManagedBlocker can help to mitigate the adverse effects of blocking operations by allowing the pool size to be temporarily extended.

Choosing the right executor is, therefore, a key architectural point that directly impacts throughput, latency, and stability.

7.2 Reentrancy traps and serialization of signals

Another critical issue is the question of how the onNext, onError and onComplete signals are delivered. The Reactive Streams specification specifies that these signals must be called sequentially and not simultaneously, so they must be serialised. Nevertheless, in practice, parallel calls arise due to your own implementations or clumsy synchronisation. This quickly leads to errors that are difficult to reproduce, the so-called “Heisenbugs”.

Therefore, a processor or subscriber must ensure that he is not confronted with onNext calls from several threads at the same time. This can be achieved through synchronisation, using queues or by delegation to an executor, which processes the signals one after the other.

7.3 Virtual Threads (Loom) vs. Reactive

With Project Loom, Java has introduced a new model: Virtual Threads. These enable the creation of millions of lightweight threads that efficiently support blocking operations. This blurs the classic line between blocking and non-blocking code. The question, therefore, arises: Do we still need reactive streams with complex backpressure at all?

The answer is: Yes, but differentiated. While virtual threads make it easier to handle multiple simultaneous operations, they do not automatically resolve the issue of uncontrolled data production. Even with Virtual Threads, a subscriber must be able to specify the number of elements they can process. Backpressure, therefore, remains an important concept. However, virtual threads can help simplify the implementation of subscriber logic, as blocking processing steps now scale much better.

Error & Completion Scenarios

In every reactive data stream, the question arises as to how to deal with errors and the regular closing. The Flow API provides clear semantics for this: Each stream ends either with a successful completion (onComplete) or with an error (onError). This simple but strict model ensures that a subscriber knows exactly what state the data flow is in at all times.

8.1 Semantics of onError and onComplete

The onError and onComplete methods mark the endpoint of a data stream. Once one of these methods has been called, no further data may be sent through onNext. This contract is crucial because it guarantees consistency: a subscriber can rest assured that they won’t have to process any new items at the end.

  • onComplete means that all elements have been successfully submitted and the source has been exhausted. Examples include reading a file or playing back a finite list of messages.
  • onError signals that an error occurred during processing. This error is passed on to the subscriber as an exception so that he can react in a targeted manner – for example, through logging, restarts, or emergency measures.

It is important  to note that onError and onComplete are mutually exclusive. So it must never happen that a stream first breaks off with an error and then signals a regular end.

8.2 Restarts and Retry Mechanisms

In many use cases, it is not sufficient to simply terminate a stream at the first error. Instead, they want to try to resume processing. Classic examples are network requests or database access, which can fail temporarily.

There is no built-in retry functionality in the flow API itself, but it can be implemented at the processor level. For example, after an error, a processor could decide to restart the request or skip the faulty record. It is important to handle the state clearly: A new onSubscribe may only take place when a new data stream is started. For more complex scenarios, patterns such as circuit breaker or retry with exponential backoff are ideal.

A simple example could be a subscriber who raises a counter on the first error and retries the faulty operation a maximum of three times. Only if an error still occurs after these retries does it finally pass the error on to the downstream:

This simple pattern is of course greatly simplified, but it clarifies the principle of a repeated attempt in the event of temporary errors.

8.3 Idem potency and exactly-once processing

A central problem with restarts is determining whether operations can be carried out more than once. Many systems are only robust if their operations are idempotent – that is, they produce the same result when executed multiple times. An example is writing a record with a specific key to a database: even if this process is repeated, the final state remains the same.

If idempotency is absent, there is a risk of double processing, which can lead to incorrect results or inconsistencies. Therefore, it is good practice to prefer idempotent operations or to ensure that data is not processed multiple times through additional mechanisms, such as transaction IDs, deduplication, or exactly-once processing.

A simple example: Suppose a processor processes orders and passes each order with a unique ID to an OrderService, which stores it. Even if the same record passes through the processor twice, the ID ensures that it only exists once in the service. This means that the operation remains idempotent.

A small Java example with a processor and JUnit5Test might look like this:

The test shows that despite the publication of order-1 being repeated twice, only one instance is sent downstream, thanks to DeduplicateProcessor. The idempotency logic is thus in the processor, not in the subscriber.

Interoperability & Integration

A key feature of the Flow API is its openness to integration. The API itself is minimal and only defines the basic contracts for publisher, subscriber, subscription and processor. This allows it to be used both standalone and combined with other reactive libraries.

9.1 Integration with Reactive Frameworks

Many established reactive frameworks, such as Project Reactor, RxJava, or Akka Streams, are based on the same fundamental ideas as the Flow API and are closely tied to the official Reactive Streams specification. They offer either direct adapters or interfaces that can be used to integrate publishers, subscribers and, in particular, your own processor implementations. This means that self-developed building blocks can be easily embedded in complex ecosystems without being tied to a specific framework. A central example is the helper class FlowAdapters in the JDK, which provides conversion methods between java.util.concurrent.Flow and org.reactivestreams.Publisher. This enables seamless integration of your own JDK-based publishers or processors with Reactor or RxJava operators without breaking the contracts. In practice, this means that a processor implemented with Flow can be integrated directly into a Reactor flux or fed from an RxJava observable, allowing existing pipelines to be expanded or migrated step by step.

A simple example shows the use of FlowAdapters. This example can also be secured with a small JUnit5 test:

This test shows that a SubmissionPublisher can be successfully turned into a Reactive Streams publisher and back again via FlowAdapters. The message “Hello world” reaches the subscriber correctly at the end.

9.2 Use in classic applications

The Flow API can also be used sensibly in non-reactive applications. For example, data streams from a message queue or a websocket can be distributed to internal services via a SubmissionPublisher. Legacy systems that have previously relied on polling benefit from distributing and processing events in real-time. This enables modern, reactive architectures to be integrated into existing applications incrementally.

A simple example illustrates the connection with a legacy service:

This small program simulates use in a classic application: Instead of polling, messages from an external source are immediately forwarded to the LegacyService , which can continue to work unchanged. The Flow API serves as a bridge between modern event processing and existing logic.

9.3 Bridge to Java Streams

A common question is how the Flow API compares to or even connects to classic Java Streams (the Stream API). While streams typically process finite amounts of data in the pull model, flow works with potentially infinite sequences in the push model. Nevertheless, a combination is possible: A processor could cache incoming data and feed it into a Java stream. Conversely, the results of a stream can be brought back into a reactive context via a publisher. This creates a bridge between batch-oriented and event-driven processing.

A small JUnit5 example demonstrates this bridge: Here, elements are transferred to a stream via a SubmissionPublisher and then summed.

In this example, the list acts as a buffer and allows the transition from the asynchronous push model (flow) to the synchronous pull model (stream). In this way, both worlds can be elegantly combined. However, the topic is far more extensive and will be examined in detail in a separate article.

Result

The Flow API in java.util.Concurrent shows how Java provides a clean, standardised basis for reactive data streams. While Java Streams are intended for finite, synchronous pull scenarios, Flow addresses the challenges of continuous, infinite and asynchronous data sources. Core concepts include backpressure, precise signal semantics (onNext, onError, onComplete), and the dual function of the Flow.Processor create the basis for stable, extensible pipelines.

The patterns shown – from map and filter processors to batching and throttling – make it clear that robust processing chains can be built with little code. Supplemented by retry strategies, idempotency and interoperability with established reactive frameworks, a toolbox is created that is suitable for simple learning examples as well as for upscaled systems.

This makes it clear: If you work with event streams in Java, you can’t get around the Flow API. It bridges the gap between classic streams, modern reactive programming and future developments such as virtual threads. It is crucial to consistently adhere to the principles of backpressure, clean use of resources and clear signal guidance. This results in pipelines that are not only functional, but also durable, high-performance and safe.

How and why to use the classic Observer pattern in Vaadin Flow

1. Introduction and motivation

The observer pattern is one of the basic design patterns of software development and is traditionally used to decouple state changes and process them. Its origins lie in the development of graphical user interfaces, where a shift in the data model required synchronising several views immediately, without a direct link between these views. This pattern quickly established itself as the standard solution to promote loosely coupled architectures.

At its core, the Observer Pattern addresses the challenge that events or state changes do not remain isolated; instead, they must be processed by different components. This creates an asynchronous notification model: The subject informs all registered observers as soon as its state changes. The observers react to this individually, without the subject having to know their concrete implementations.

The source code with the examples can be found in the following git-repo: https://github.com/Java-Publications/Blog—Vaadin—How-to-use-the-Observer-Pattern-and-why

In modern software architectures, characterised by interactivity, parallelism, and distributed systems, this basic principle remains highly relevant. It continues to be used in the field of UI development, as user interactions and data flows must be handled dynamically and in a reactive manner. Frameworks such as Vaadin Flow take up this idea, but go beyond the classic implementation by providing mechanisms for state matching, server-side synchronisation and client-side updates.

The motivation to apply the Observer Pattern in the context of Vaadin Flow lies in comparing proven yet simple patterns and their further development within a modern UI framework. This not only sharpens the understanding of loose coupling and event handling, but also creates a practical reference to today’s web applications.

2. The classic observer pattern

The classical observer pattern describes the relationship between a subject and a set of observers. The subject maintains the current state and automatically informs all registered observers of any relevant change. This achieves a loose coupling: the subject only knows the interface of its observers, but not their concrete implementation or functionality.

In its original form, according to the “Gang of Four” definition, the pattern consists of two central roles. The subject manages the list of its observers and provides methods of logging in and out. As soon as the internal state changes, it calls a notification method for all observers. The observers implement an interface through which they are informed and can define their own reaction. This allows state changes to be propagated without creating complex coupling or cyclic dependencies.

To avoid outdated dependencies and to maintain control over the API, an independent, generic variant with its own interfaces is shown below. The aim is to demonstrate how it works in a JUnit 5 test without the main method. The output is a logger via HasLogger from the package com.svenruppert.dependencies.core.logger.

In the example, the company’s own API defines two lean interfaces: Subject<T> manages observers and propagates changes, while Observer<T> reacts to updates. The implementation of the Subject uses a CopyOnWriteArrayList to handle concurrent logins and logoffs during notification robustly. The JUnit5Test demonstrates the expected behaviour: By setting the temperature twice, notifications are triggered twice. The temperature display logs the values via HasLogger and retains the last observed value for assertion. This demonstrates the basic semantics of the Observer Pattern – loose coupling, clear responsibilities and event-driven updating – in a modern, framework-free Java variant.

3. Observer Pattern in Vaadin Flow

While the classic Observer Pattern has its origins in desktop programming, Vaadin Flow elevates the concept to a higher level of abstraction. Since Vaadin operates on the server side and manages the UI state centrally, synchronisation between the data model, user interface, and user interactions is an integral part of the framework. Events and listeners take on the role of the observer mechanism.

A central element is the ValueChangeListener, which is automatically triggered when changes are made to input components such as text fields or checkboxes. Here, the UI component acts as a subject, while listeners represent the observer. As soon as the user makes an input, an event is generated, and all registered listeners are informed. All the developer has to do is implement the desired response without worrying about chaining event flows.

Additionally, Vaadin provides a powerful tool with the Binder, which enables bidirectional data binding between the data model and UI components. Changes to a field in the form are automatically propagated to the bound object, while changes in the object are immediately visible in the UI elements. The binder thus adopts a bidirectional observation that goes beyond the possibilities of the classic observer pattern.

Another example of the observer pattern’s implementation in Vaadin Flow is the EventBus mechanism, which enables components or services to exchange information about events in a loosely coupled manner. Here, it becomes clear that Vaadin adopts the basic idea of the pattern, but expands it in the context of a web application to include aspects such as thread security, lifecycle management, and client-server synchronisation.

The strength of Vaadin Flow lies in the fact that developers no longer have to implement observer interfaces explicitly; instead, they can establish observation relationships via declarative APIs, such as addValueChangeListener, addClickListener, or via binder bindings. This does not abolish the pattern, but integrates it deeply into the architecture and adapts it to the special requirements of a server-side web UI.

Vaadin Flow implements the basic idea of the Observer Pattern along two levels. At the UI level, component-specific events (e.g., ValueChangeEvent, ClickEvent) exist that are registered via listeners and trigger changes to the representation or state model. This mechanism corresponds to a finely granulated, component-internal observer and is conveyed via an internal EventBus for each component. At the application level, a domain-level observation structure is also recommended to model state changes outside the UI and to mirror them in the UI in a decoupled manner. This allows long-lived domain objects and short-lived UII punches to be cleanly separated.

A minimal example exclusively with Vaadin on-board tools shows the observation of UIC conditions as well as the binding to a model via binder:

Here, addValueChangeListener(…), the role of the observer at the component level, while the binder synchronises the changes between the input field and the domain object. Without additional infrastructure, this creates a transparent, declarative observation chain from the UI to the model and back to the UI. It is important to note that the button does not trigger a new save, but reads the model state that has already been synchronised by the binder and makes it visible. It thus serves as an action-level observer that confirms and logs the current consistency between UI and model.

4. Differences between classic Observer and Vaadin Flow

The comparison between the classic Observer Pattern and its implementation in Vaadin Flow reveals that both approaches share the same basic idea, but operate at different levels of abstraction. While the classic pattern relies on purely object-oriented structures – subject and observer communicate directly via defined interfaces – Vaadin Flow shifts observation to a framework-supported environment with integrated event control.

A significant difference lies in life cycle management. In the classic observer, subjects and observers remain the responsibility of the developer; Registration and deregistration must be done manually, otherwise there is a risk of memory leaks. Vaadin Flow integrates observation into the lifecycle of UI components. Listeners are registered when a component is created and automatically resolved when the component is removed, reducing administrative overhead.

ThreadSecurity also differs significantly. The classic observer pattern works in a local context and has no UI thread binding. Vaadin Flow, on the other hand, must guarantee that UI updates are done exclusively in the UI thread. This forces the use of mechanisms such as UI.access(…)as soon as events from foreign threads are processed.

Another difference concerns the direction and range of the synchronisation. In the classic model, notifications are limited to the objects involved. Vaadin Flow extends this principle to client-server communication: changes to the UI are automatically synchronised to the server, while server-side state changes are, in turn, made visible in the UI on the client side.

Finally, the granularity of the observation varies. Classic observers usually react to gross changes in state. Vaadin Flow, on the other hand, offers a variety of specialised event types (e.g. ValueChange, Click, Attach/Detach) that allow for very finely tuned reactions. In combination with the binder , a bidirectional coupling between model and UI is created that goes far beyond the original idea of the pattern.

In summary, the classic observer pattern provides the theoretical basis. Still, Vaadin Flow abstracts and expands it by integrating lifecycle, thread security, and client-server synchronisation, thereby enabling a higher degree of robustness and productivity.

5. When does the classic observer pattern still make sense?

Although Vaadin Flow already integrates many observation mechanisms, there are scenarios in which the classic observer pattern can also be used sensibly in modern Vaadin applications. The crucial point here is the separation between UI logic and domain logic. While listeners and binders in Vaadin handle the synchronisation between user input and UI components, events often arise in the business layer that must be managed independently of the UI.

A typical field of application is internal domain events. For example, a background process can receive a new message, complete a calculation, or import data from an external system. These events should not be directly bound to UI components; instead, they should be distributed throughout the system. Here, the classic observer pattern is suitable for informing interested services or components of such changes without creating a direct link.

The pattern also plays a role in the context of integrations with external systems. When a REST service or messaging infrastructure feeds events into the system, the observer pattern can be used to propagate those events within the domain first. Only in the second step is it decided whether and how this information is passed on to a Vaadin UI. This keeps the dependency on the UI low, and the domain layer remains usable outside of a UI context.

Another reason for using the classic observer pattern is testability and reusability. Unit testing at the domain layer is easier to perform when observation mechanisms do not depend on Vaadin-specific APIs. This allows complex interactions to be tested in isolation before they are later wired to the UI.

In summary, the classic observer pattern complements the mechanisms available in Vaadin Flow for UI-independent, domain-internal events or integrations with external sources. It enables loose coupling, increases testability and ensures clear demarcations between layers – a principle that is a decisive advantage, especially in more complex applications.

6. Example: Combination of Observer Pattern and Vaadin Flow (with external producer)

An efficient variant arises when messages are not generated by the UI itself, but come from an external producer outside the UI life cycle. Typical sources are background processes, REST integrations or message queues. The Observer Pattern ensures that these events can be distributed to all interested components, regardless of the UI.

The following example extends the previous construction: A MessageProducer periodically generates messages in a separate thread and passes them to the MessageService. The Vaadin view remains a pure observer, reflecting only the messages in the UI.

This structure creates a complete message flow producer → domain → UI. The domain layer is independent of Vaadin and can be tested in isolation. The UI remains limited to the display and updates itself exclusively in the UI thread.

The new messages appear every two seconds, as the MessageProducer is configured  in the background with scheduleAtFixedRate. When it starts, it immediately generates a message and then periodically generates a new one every two seconds. These are  distributed to the registered observers  via the MessageService. Important: For server-side changes to be reflected in the browser without user interaction, ServerPush or Polling must be enabled. In this example, @Push on the View ensures that ui.access(…) actively sends the changes to the client. Alternatively, polling (e.g. @Poll(2000) or UI#setPollInterval). The view receives the events and sets the text in the div using UI.access(…) in UI Thread, so that the most recent message is visible.

This uses the Observer Pattern to integrate external events into a Vaadin Flow application reliably.

7. Best Practices and Pitfalls

The integration of the classic Observer Pattern into a Vaadin Flow application brings both advantages and specific challenges. To ensure a robust and maintainable architecture, several best practices should be considered.

Observer lifecycle management. A common mistake is to leave Observer permanently registered in the service, even if the associated UI component has already been destroyed. This leads to memory leaks and unexpected calls to views that no longer exist. Therefore, observers in Vaadin views should always be registered inonAttach and removed in onDetach. In this way, the observation remains clearly linked to the life cycle of the UI.

Use of suitable data structures
Instead of simple lists, it is recommended to use concurrent, duplicate-free data structures, such as ConcurrentHashMap or CopyOnWriteArraySet, for managing observers. This avoids duplicate registrations and ensures thread safety, even if multiple threads add or remove observers at the same time.

Thread security and UI updates. Since external events are often generated in separate threads, it is imperative to install UI updates in Vaadin viaUI.access(…) be performed. Otherwise, there is a risk of race conditions and exceptions, because the UI components may only be changed from within the UI thread. Additionally, server push or polling should be enabled to ensure that changes are sent to the client without user interaction.

Domain and UI demarcation. The domain layer should remain completely free of Vaadin-specific dependencies. This ensures that business logic can be tested and reused outside of the UI context. The UI only registers itself as an observer and takes over the representation of the events delivered by the domain.

Error handling and logging
Events should be handled robustly to prevent individual errors from interrupting the entire notification flow. Clean logging (e.g. via a common HasLogger interface) facilitates the analysis and traceability of the event flow.

Summary. The main pitfalls lie in improper lifecycle management, a lack of thread security, and overly tight coupling between the UI and domain. When these aspects are taken into account, the combination of the Observer Pattern and Vaadin Flow enables a clear separation of responsibilities, testable business logic, and a reactive, user-friendly interface.

8. Conclusion

A look at the Observer Pattern in combination with Vaadin Flow reveals that it remains a relevant and compelling design pattern, whose benefits extend beyond classic desktop or console applications. In its original form, it provides apparent decoupling of state changes and their processing, thus creating a clean basis for loosely coupled architectures.

Vaadin Flow abstracts and extends this approach by integrating observation mechanisms directly into the lifecycle of UI components, providing a variety of specialised events, and automating synchronisation between server and client. This relieves the developer of many tasks that could still be solved manually in the classic observer pattern, such as memory management or thread security.

Nevertheless, the classic observer pattern remains essential in areas where UI-independent events occur or external systems are connected. The combination of both approaches – a clearly defined domain layer with observers and a Vaadin UI based on it – creates an architecture that is both loosely coupled, testable, and extensible.

Overall, it can be said that the Observer Pattern forms the theoretical basis, while Vaadin Flow provides the practical implementation for modern web applications. Those who consciously combine both approaches benefit from robust and flexible systems that are prepared for future requirements.

Short links, clear architecture – A URL shortener in Core Java

A URL shortener seems harmless – but if implemented incorrectly, it opens the door to phishing, enumeration, and data leakage. In this first part, I’ll explore the theoretical and security-relevant fundamentals of a URL shortener in Java – without any frameworks, but with a focus on entropy, collision tolerance, rate limiting, validity logic, and digital responsibility. The second part covers the complete implementation: modular, transparent, and as secure as possible.

1.1 Motivation and use cases

In an increasingly fragmented and mobile information world, URLs are not just technical addressing mechanisms; they are central building blocks of digital communication. Long and hard-to-remember URLs are a hindrance in social media, emails, or QR codes, as they are not only aesthetically unappealing but also prone to errors when manually entered. URL shorteners address this problem by generating compact representations that point to the original target address. In addition to improved readability, aspects such as statistical analysis, access control, and campaign tracking also play a key role.

Initially popularised by services like TinyURL or bit.ly, URL shorteners have now become integrated into many technical infrastructures – from marketing platforms and messaging systems to IoT applications, where storage and bandwidth restrictions play a significant role. A shortened representation of URLs is also a clear advantage in the context of QR codes or limited character sets (e.g., in SMS or NFC data sets).

A URL shortener is not a classic forwarding platform and is conceptually different from proxy systems, link resolvers, or load balancers. While the latter often operate at the transport or application layer (Layer 4 or Layer 7 in the OSI model) and optimise transparency, availability, or performance, a shortener primarily pursues the goal of simplifying the display and management of URLs. Nevertheless, there are overlaps, particularly in the analysis of access patterns and the configuration of redirect policies.

In this work, a minimalist URL shortener is designed and implemented. It deliberately avoids external frameworks to implement the central concepts in a comprehensible and transparent manner in Core Java. The choice of Java 24 enables the integration of modern language features, such as records, sealed types, and virtual threads, into a secure and robust architecture.

1.3 Objective of the paper

This paper serves a dual purpose: on the one hand, it aims to provide a deep technical understanding of the functionality and challenges associated with a URL shortener. On the other hand, it serves as a practical guide for implementing such a service using pure Java—that is, without Spring, Jakarta EE, or external libraries.

To this end, a comprehensive architecture will be developed, implemented, and continually enhanced with key aspects such as security, performance, and extensibility. The focus is deliberately on a system-level analysis of the processes to provide developers with a deeper understanding of the interaction between the network layer, coding strategies, and persistent storage. The goal is to develop a viable model that can be utilised in both educational contexts and as a basis for productive services.

2. Technical background

2.1 URI, URL and URN – conceptual basics

In everyday language, terms such as “URL” and “link” are often used synonymously, although in a technical sense, they describe different concepts.URI (Uniform Resource Identifier)refers to any character string that can uniquely name or locate a resource. A URL (Uniform Resource Locator) is a special form of a URI that not only identifies but also describes the access path, for example, through a protocol such as https, ftp, or mailto. A URN (Uniform Resource Name), on the other hand, names a resource persistently without referring to its physical address, such as urn:isbn:978-3-16-148410-0.

In the context of URL shorteners, URLs are exclusively concerned with accessible paths, typically via HTTP or HTTPS. The challenge is to transform these access paths in a way that preserves their semantics while reducing their representation.

2.2 Principles of address shortening

The core idea of ​​a URL shortener is to replace a long URL string with a shorter key that points to the original address via a mapping. This mapping is done either directly in a lookup store (e.g., hash map, database table) or indirectly via a computational method (e.g., a hash function with collision management).

The goal is to use the redundancy of long URLs to map their entropy to a significantly shorter string. This poses a trade-off between collision-freeness, brevity, and readability. Conventional methods are based on encoding unique keys in a Base62 alphabet ([0-9a-zA-Z]), which offers 62 states per character. Just six characters can represent over 56 billion unique URLs—sufficient for many productive applications.

The shortcode acts as the primary key for address resolution. It is crucial that it is stable, efficiently generated, and as challenging to guess as possible to prevent misuse (e.g., brute-force enumeration).

2.3 Entropy, collisions and permutation spaces

A key aspect of URL shortening is the question of how many different short addresses a system can actually generate. This consideration directly depends on the length of the generated shortcuts and their character set. Many URL shorteners use a so-called Base62 alphabet. This includes the ten digits from zero to nine, the 26 lowercase letters, and the 26 uppercase letters, for a total of 62 different characters.

For example, if you generate abbreviations with a fixed length of six characters, you get a combinatorial space in which over 56 billion different character strings are possible. Even with this relatively short number of characters, billions of unique URLs can be represented, which is more than sufficient for many real-world applications. For longer abbreviations, the address space grows exponentially.

But the sheer number of possible combinations is only one aspect. How these shortcuts are generated is equally important. If the generation is random, it is essential to ensure that no duplicate codes are created – so-called collisions. These can be managed either by checking for their existence beforehand or by deterministic methods such as hash functions. However, hash methods are not without risks, especially under heavy load: The more entries there are, the higher the probability that two different URLs will receive the same short code, especially if the hash function has not been optimised for this use case.

Another criterion is the distribution of the generated shortcuts. A uniform distribution in the address space is desirable because, on the one hand, it reduces the risk of collisions, and on the other hand, it increases the efficiency of storage and retrieval mechanisms – for example, in sharding for distributed systems or caching in high-traffic environments. Cryptographically secure random numbers or specially designed generators play a crucial role here.

Overall, it can be said that the choice of alphabet, the length of the abbreviations and the way they are generated are not just technical parameters, but fundamental design decisions that significantly influence the security, efficiency and scalability of a URL shortener.

3. Architecture of a URL shortener

The architecture of a URL shortener is surprisingly compact at its core, but by no means trivial. Although its basic function is simply to link a long URL with a short alias, numerous technical and conceptual decisions arise in the details. These include data storage, the structure of API access, concurrency behaviour, and security against misuse. This chapter explains the central components and their interaction, deliberately avoiding external frameworks. Instead, the focus is on a modular, transparent structure in pure Java.

At the heart of the system is a mapping table – typically in the form of a map or a persistent key-value database – that uniquely assigns each generated short code to its corresponding original URL. This structure forms the backbone of the shortener. Crucially, this mapping must be both efficiently readable and consistently modifiable, especially under load or when accessed concurrently by multiple clients.

A typical URL shortener consists of three logically separate units: an input endpoint for registering a new URL, a redirection endpoint for evaluating a short link, and a management unit that provides metadata such as expiration times or access counters. In a purely Java-based solution without frameworks, network access is provided via the HTTP server introduced in Java 18.  com.sun.net.httpserver package. This allows you to define REST-like endpoints with minimal overhead and to communicate with HttpExchange objects.

There are various options for storing mappings. In-memory structures, such as ConcurrentHashMap, offer maximum speed but are volatile and unsuitable for productive applications without a backup mechanism. Alternatively, file-based formats, relational databases, or object-oriented stores such as EclipseStore can be used. This paper will initially work with volatile storage to illustrate the basic logic. Persistence will be added modularly later.

Another key aspect concerns concurrency behaviour. Since URL shorteners are typically burdened by a large number of read accesses, for example, when calling short links, the architecture must be designed to allow concurrent access to the lookup table without locking conflicts. The same applies to the generation of new shortcuts, which must be atomic and collision-free. Java 24 introduces modern language tools, including virtual threads and structured concurrency, which can be utilised to manage server load in a more deterministic and scalable manner.

Last but not least, horizontal extensibility plays a role. A cleanly decoupled design allows the shortener to be easily transferred to distributed systems later. For example, the actual URL resolver can be operated as a stateless service, while data storage is outsourced to a shared backend. Caching strategies and load balancing can also be integrated much more easily in such a setup.

In summary, a URL shortener is much more than a simple string replacement. Its architecture must be both efficient, robust, and extensible—properties that can be easily achieved through a modular structure in pure Java.

4. Implementation with Java 24

4.1 Project structure and module overview

The implementation of the URL shortener follows a modular structure that supports both clarity in the source code and testability, as well as extensibility. The project is structured as a Java module and leverages the capabilities of the Java Platform Module System (JPMS). The goal is to separate the core functionality—that is, the management of URL mappings—from the network layer and persistence. This keeps the business logic independent of specific storage or transport mechanisms.

At the centre is a module called shortener.core, which contains all domain-specific classes: for example, the ShortUrlMapping, the UrlEncoder, as well as the central UrlMappingStore interface with a simple implementation in memory. A module shortener.http, which is based on Java’s internal HTTP server. It implements the REST endpoints and utilises the core module’s components for actual processing. Additional optional modules, such as those for persistence or analysis, can be added later.

To organise the code, a directory structure that clearly reflects the module and layer boundaries is recommended. Within the modules, a distinction should be made between api, impl, util and, if necessary, service.

4.2 URL Encoding: Hashing, Base62 and Alternatives

A central element of the shortener is the mechanism for generating short, unique codes. This implementation uses a hybrid method that generates a consecutive, atomic sequence number and converts it into a human-readable format using a Base62 encoder.

This choice has two advantages: First, it is deterministic and avoids collisions without the need for complex hash functions. Second, generated codes can be efficiently serialised and are easy to read, which is particularly relevant in marketing or print contexts. Alternatively, cryptographic hashes such as SHA-256 can be used when unpredictability and integrity protection are essential, for example, for signed links or zero-knowledge schemes.

The Base62 encoder is implemented as a pure utility class that encodes integer values ​​into a character string, where the alphabet consists of numbers and letters. Inverse decoding is also provided in case bidirectional analysis is required in the future.

4.3 Mapping Store: Interface, Implementation, Synchronisation

For managing URL mappings, a clearly defined interface called UrlMappingStore provides methods for inserting new mappings, resolving short links, and optionally managing metadata. The default implementation, InMemoryUrlMappingStore, is based on a ConcurrentHashMap and utilises AtomicLong for sequence number generation.

This simple architecture is completely thread-safe and allows parallel access without external synchronisation mechanisms. The implementation can be replaced at any time with a persistent variant, for example, based on flat file storage or through integration with an object-oriented storage system such as EclipseStore.

This separation keeps the application core stable while treating storage as a replaceable detail—a classic example of the dependency inversion principle in the spirit of Clean Architecture.

4.4 REST API with pure Java (HTTP server, handler, routing)

The REST interface is implemented exclusively with the built-in tools of the JDK. Java provides the package com.sun.net.httpserver, which offers a minimalistic yet powerful HTTP server ideal for lean services. For the implementation of the API, a separate HttpHandler is defined that responds to specific routes, such as /shorten for POST requests and /{code} for forwarding.

The implementation is based on a clear separation between parsing, processing, and response generation. Incoming JSON messages are parsed manually or with the help of simple helper classes, without the need for external libraries. HTTP responses also follow a minimalist format, characterised by structured status codes, simple header management, and UTF-8-encoded bodies.

Routing is handled by a dispatcher class, which selects the appropriate handler based on the request path and HTTP method. Later extensions, such as CORS, OPTIONS handling, or versioning, are easily possible.

4.5 Error handling, logging and monitoring

In a productive environment, robust error handling is essential. The implementation distinguishes between systematic errors (such as invalid inputs or missing short codes) and unexpected runtime errors (such as IO problems or race conditions). The former are reported with clear HTTP status codes, such as 400 (Bad Request) or 404 (Not Found). The latter leads to a generic 500 Internal Server Error, with the causes being logged internally.

For logging, the JDK’s own java.util.logging This allows for platform-independent logging and can be replaced with SLF4J-compatible systems if needed. Monitoring metrics such as access counts, response times, or error statistics can be made accessible via a separate endpoint or JMX.

5. Security aspects

5.1 Abuse opportunities and protection mechanisms

A URL shortener can easily be used to obscure content. Attackers deliberately exploit the shortening to redirect recipients to phishing sites, malware hosts, or dubious content without the target address being immediately visible. This can pose significant risks, especially for automated distributions via social networks, chatbots, or email campaigns.

An adequate protection mechanism consists of automatically validating all target addresses upon insertion, for example, through syntactical URL checks, DNS resolution, and optionally through a background query (head request or proxy scan) that ensures that the target page is accessible and non-suspicious. Such checks should be modular so that they can be activated or deactivated depending on the environment (e.g., offline operation). Additionally, logging should be performed every time a short link is accessed, making it easier to identify patterns of abuse.

5.2 Rate limiting and IP-based throttling

Another risk lies in excessive use of the service, be it through botnets, targeted enumeration, or simple DoS behaviour. A robust URL shortener should therefore have rate limiting that restricts requests within a given time slot. This can be global, IP-based, or per-user, depending on the context.

In a Java implementation without frameworks, this can be achieved, for example, via a ConcurrentHashMap that maintains a timestamp or counter buffer for each IP address. If a threshold is exceeded, the connection is terminated with a status code of 429 Too Many Requests rejected. This simple throttling can be supplemented with leaky bucket or token bucket algorithms if necessary to achieve a fairer distribution over time. For productive use, logging of critical threshold violations is also recommended.

5.3 Validity period and deletion concepts

Not every short link should remain valid forever. A configurable validity period is essential, especially for security-critical applications, such as temporary document sharing or one-time authentication. A URL shortener should therefore offer the option of defining expiration times for each mapping.

On a technical level, it is sufficient to assign an expiration date to each mapping, which is checked during the lookup. When accessing expired short links, either an error status, such as 410 Gone, is displayed, or the user is redirected to a defined information page. Additionally, there should be periodic cleanup mechanisms that remove expired or unused entries from memory, such as through a time-controlled cleanup process or lazy deletion upon access.

5.4 Protection against enumeration and information leakage

An often overlooked attack vector is the systematic scanning of the abbreviation space – for example, by automated retrieval of /aaaaaa until /zzzzzz. If a URL shortener delivers valid links without any protection mechanisms, potentially confidential information about the existence and use of links can be leaked.

An adequate protection consists in making the shortcuts themselves non-deterministic – for example, by using cryptographically generated, unpredictable tokens instead of continuous sequences. Additionally, access restrictions can be introduced, allowing only authenticated clients to access certain short links or excluding specific IP ranges. The targeted obfuscation of error responses – for example, by consistently issuing 404 Not Found even with blocked or expired abbreviations – makes analysis more difficult for attackers.

A further risk arises when metadata such as creation time, number of accesses, or request origin is exposed unprotected via the API. Such information should only be accessible to authorised users or administrative interfaces and should never be part of the public API output.

6. Performance and optimisation

6.1 Access times and hash lookups

The most common operation in a URL shortener is resolving a shortcode into its corresponding original URL. Since this is a classic lookup operation, the choice of the underlying data structure is crucial. In the standard implementation, a ConcurrentHashMap, which is optimised in Java 24, has fine-grained locking. This offers nearly constant access times – even under high concurrency – and is therefore ideal for read-intensive workloads, such as those typical of a shortener.

The latency of such an operation is in the range of a few microseconds, provided the lookup table is stored in main memory and no additional network or IO layers are involved. However, if data storage is outsourced to persistent systems, such as a relational database or a disk-based key-value store, the access time increases accordingly. Therefore, it is recommended to cache frequently accessed entries – either directly in memory or via a dedicated cache layer.

Performance also plays a role in the creation of new abbreviations. This is where sequence number generation using AtomicLong is used, providing a thread-safe, low-contention solution for linear ID assignment. Combined with Base62 encoding, this creates a fast, predictable, and collision-free process.

6.2 Memory usage and garbage collection

Since a URL shortener must manage a growing number of entries over a longer period, it is worthwhile to examine its storage behaviour. ConcurrentHashMap. While this results in fast access times, it also means that all active mappings remain permanently in memory—unless cleanup is implemented. A simple mapping structure consisting of a shortcode, original URL, and an optional timestamp requires several hundred bytes per entry, depending on the JVM configuration and string length.

With several million entries, heap usage can reach several gigabytes. To improve efficiency, care should be taken to use objects sparingly. For example, common URL prefixes (e.g. https://) are replaced with symbolic constants. Records instead of classic POJOs also help reduce object size and minimise GC load.

In the long term, it is recommended to introduce an active or passive cleanup mechanism, such as TTL-based eviction or access counters, to specifically remove rarely used entries. WeakReference or soft caching should be considered with caution, since the semantics of such structures do not always lead to expected behaviour in the server context.

6.3 Benchmarking: Local tests and load simulation

Systematic benchmarking is essential for objectively evaluating the performance of a URL shortener. At a local level, this can be achieved with simple Java benchmarks that measure sequence number generation, lookup time, and code distribution quality. Tools such as JMH (Java Microbenchmark Harness) can also be used. Although external tools are not used in this paper, a manual microbenchmarking approach using System.nanoTime and a targeted warm-up can provide valuable insights.

For more realistic tests, a load simulation with HTTP clients is suitable, for example, using simple JDK-based multi-thread scripts or tools such as curl. In particular, behaviour under high concurrent access load should be observed, both in terms of response times and resource consumption. Behaviour in the event of failed requests, rapid-fire access, or expired links should also be explicitly tested.

The goal of such benchmarks is not only to validate the maximum transaction rate, but also to verify stability under continuous load. A robust implementation should not only be high-performance but also deterministic in its response behaviour and resistant to out-of-memory errors. Optional profiling—for example, using JDK Flight Recorder—can reveal further optimisation potential.

7. Expansion options and variants

7.1 Custom aliases

A frequently expressed wish in practice is the ability to not only use automatically generated short links, but also to assign custom aliases – for example, for marketing campaigns, internal documents, or individual redirects. A custom alias, such as /travel2025 is much easier to remember than a random Base62 token and can be integrated explicitly into communication and branding.

Technically speaking, this expands the mapping store’s responsibility. Instead of only accepting numerically generated keys, the API must verify that a user-defined alias is syntactically valid, not already in use, and not reserved. A simple regex check, supplemented by a negative list for reserved terms (e.g. /admin, /api), is sufficient to get started. This alias must then be treated equally to the automatically generated codes when stored.

This creates new failure modes, for example, when a user requests an alias that already exists. Such cases should be handled consistently with a 409 Conflict. The API can optionally suggest alternative names—a small convenience feature with a significant impact on the user experience (UX).

7.2 Access counting and analytics

A functional URL shortener is more than just a redirection tool—it’s also an analytics tool. Tracking how often, when, and from where a short link was accessed is particularly relevant in the context of campaigns, product pages, or documented distribution.

To implement this functionality, each successful resolution of a short link must be saved as an event, ​​either by simply incrementing a counter or by fully logging with a timestamp, IP address, and user agent. For the in-memory variant, an additional AtomicLong or a metric structure aggregated via a map. Alternatively, detailed access data can be persisted in a dedicated log file or an external analytics module.

The evaluation can be performed either synchronously via API endpoints (e.g.,/stats/{alias}) or asynchronously via export formats such as JSON, CSV, or Prometheus metrics. Integration with existing logging systems (e.g. via java.util.logging or Logstash) is easily possible.

7.3 QR-Code-Integration

For physical media, such as posters, packaging, or invitations, displaying a short link as a QR code is a useful extension. Integrating QR code generation into the URL shortener enables the direct generation of a visually encoded image of the link from the API.

Since no external libraries are used, QR code generation can be performed using a compact Java-based algorithm, such as one based on bit matrix generation and SVG output. Alternatively, a Base64-encoded PNG file can be delivered via an endpoint URL such as /qr/{alias}. The underlying data structure remains unchanged – only the representation is extended.

This feature not only enhances practical utility but also expands the service’s reach across multiple media channels.

7.4 Integration into messaging or tracking systems

In production architectures, a URL shortener typically operates in conjunction with other components. Instead, it is part of larger pipelines – for example, in email delivery, chatbots, content management systems, or user interaction tracking. Flexible integration with messaging systems such as Kafka, RabbitMQ, or simple webhooks allows every link creation or access to be transmitted as an event to external systems.

In a pure Java environment, this can be done via simple HTTP requests, log files, or asynchronous event queues. Scenarios are conceivable in which a notification is automatically sent to a third-party system for each new short link, for example, to generate personalised campaigns or for auditing purposes. Access to short links can also be mapped via events, which are subsequently statistically evaluated or visualised in dashboards.

Depending on the level of integration, it is recommended to implement a dedicated event dispatcher that encapsulates incoming or outgoing events and forwards them in a loosely coupled manner. This keeps the shortener itself lean and responsibilities clearly distributed.

A URL shortener that logs visits automatically operates within the framework of data protection law. As soon as data such as IP addresses, timestamps, or user agents are stored, it is considered personal information in the legal sense, at least potentially. In the European Union, such data falls under the General Data Protection Regulation (GDPR), which entails specific obligations for operators.

The technical capability for analytics—for example, through access counting or geo-IP analysis—should therefore not be enabled implicitly. Instead, a URL shortener should be designed so that tracking mechanisms must be explicitly enabled, ideally with clear labelling for the end user. A differentiated configuration that distinguishes between anonymised and personal data collection is strongly recommended in professional environments.

Additionally, when storing personal data, a record of processing activities must be maintained, a legal basis (e.g., legitimate interest or consent) must be specified, and a defined retention period must be established. For publicly accessible shorteners, this may mean that tracking remains deactivated by default or is controlled via consent mechanisms. The implementation of such control structures is not part of the core functionality, but is an integral part of data protection-compliant operations.

8.2 Responsibility for forwarding

Another key point is the service provider’s responsibility for the content to which the link is redirected. Even if a shortener technically only implements a redirect, legal responsibility arises as soon as the impression arises that the operator endorses or controls the target content. This is especially true for public or embedded shorteners, such as those found in corporate portals or social platforms.

The challenge lies in distinguishing between technical neutrality and de facto mediation. It is therefore advisable to integrate legal protection mechanisms into the architecture, for example, through a policy that excludes the upload of specific domains, regular URL revalidation, or the use of abuse detection systems. In the event of misuse or complaints, immediate deactivation of individual mappings should be possible, ideally via a separate administration interface.

This responsibility is not only legally relevant but also has a reputational impact: Shorteners used to spread harmful content quickly lose their credibility – and possibly also their access to platforms or search engines.

8.3 Transparency and disclosure of the destination address

A common criticism of URL shorteners is that the destination address is no longer visible to the user. This limits the ability to evaluate whether a link is trustworthy before clicking on it. From an ethical perspective, this raises the question of whether a shortener should offer a pre-check option.

Technically, this can be achieved through a special preview mode, such as via an appendage, by explicitly calling an API or HTML preview page that transparently resolves the mapping, for example, a link like https://short.ly/abc123+. Instead of redirecting immediately, the user first displays an information page that displays the original URL and redirects to the page if desired. This function can be supplemented with information about validity, access statistics, or trustworthiness.

A transparent approach to redirects not only increases user acceptance but also reduces the potential for abuse, especially among security-conscious target groups. In sensitive environments, a mandatory preview page – for example, for all non-authenticated users – can be a helpful measure.

9. Conclusion and outlook

9.1 Lessons Learned

The development of a URL shortener in pure Java, without frameworks or external libraries, has demonstrated how even seemingly trivial web services, upon closer inspection, reveal themselves to be complex systems with diverse requirements. From the basic function of address shortening to security aspects and operational and legal implications, the result is a system that must be architecturally well-structured, yet flexible and extensible.

The importance of a clear separation of responsibilities is particularly important: A stable mapping store, a deterministic encoder, a secure yet straightforward REST API, and understandable error handling form the backbone of a robust service. Modern language tools from Java 24, such as records, sealed types, and virtual threads, enable a remarkably compact, type-safe, and concurrency-capable implementation.

The conscious decision against frameworks not only maximised the learning effect but also contributed to a deeper understanding of HTTP, data storage, thread safety, and API design – a valuable perspective for developers who want to operate in a technology-independent environment.

9.2 Possible further developments (e.g. blockchain, DNSSEC)

Despite their apparent simplicity, URL shorteners represent a fascinating field for technological innovation. There are efforts to move away from centralised management of the mapping between short code and target URL, instead using decentralised technologies such as blockchain. In this case, each link is stored as a transaction, providing resistance to manipulation and historical traceability. In practice, however, this places high demands on latency and infrastructure, which is why such approaches have been used so far rarely in production.

Another development strand lies in integration with DNSSEC-based procedures. This not only signs the shortcode itself, but also cryptographically verifies the authenticity of the resolved host. This could combine trust and verification, especially in security-critical areas such as government services, banks, or certificate authorities.

AI-supported heuristics, such as those for misuse detection or memory cleanup prioritisation, also offer potential. However, the integration of such mechanisms requires a data-efficient, explainable design that is compatible with applicable data protection regimes.

9.3 Importance of URL shorteners in the context of digital sovereignty

In today’s digital landscape, URL shorteners are more than just a convenience feature; they are a valuable tool. They influence the visibility, accessibility, and traceability of content. The question of whether and how a link is modified or redirected has a direct impact on information sovereignty and transparency, and thus on digital sovereignty.

Especially in the public sector, educational institutions, or organisations with strict compliance requirements, URL shorteners should not be operated as outsourced cloud services; instead, they should be developed in-house or at least integrated in a controlled manner. A self-hosted solution not only allows complete control over data flows and access histories but also protects against censorship-like outages or data-driven tracking by third parties.

This makes the URL shortener, as inconspicuous as its function may seem, a strategic component of a trustworthy IT infrastructure. It exemplifies the question: Who controls the path of information? In this respect, a custom shortener is not just a tool, but also a statement of identity.

The next part will be about the implementation itself..

Happy Coding

Pattern from the practical life of a software developer

Builder-Pattern

The book from the “gang of four” is part of the essential reading in just about every computer science branch. The basic patterns are described and grouped to get a good start on the topic of design patterns. But how does it look later in use?
Here we will take a closer look at one pattern and expand it.



The Pattern – Builder

The builder pattern is currently enjoying increasing popularity as it allows you to build a fluent API.
It is also lovely that an IDE can generate this pattern quite quickly. But how about using this design pattern in daily life?

The basic builder pattern

Let’s start with the basic pattern, the initial version with which we have already gained all our experience.
For example, I’ll take a Car class with the Engine and List <Wheels> attributes. A car’s description is certainly not very precise, but it is enough to demonstrate some specific builder-pattern behaviours.

Now let’s start with the Car class.

public class Car {
     private Engine engine;
     private List wheelList;
     //SNIPP
 }

At this point, I leave out the get and set methods in this listing. If you generate a builder for this, you get something like the following.

public static final class Builder {
        private Engine engine;
        private List<Wheel> wheelList;
        private Builder() {
        }
        public Builder withEngine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public Builder withWheelList(List<Wheel> wheelList) {
            this.wheelList = wheelList;
            return this;
        }
        public Car build() {
            return new Car(this);
        }
    }

Here the builder is implemented as a static inner class. The constructor of the “Car” class has also been modified.

    private Car(Builder builder) {
        setEngine(builder.engine);
        wheelList = builder.wheelList;
    }

On the one hand, there has been a change from public to private, and on the other hand, an instance of the builder has been added as a method parameter.

    Car car = Car.newBuilder()
        .withEngine(engine)
        .withWheelList(wheels)

An example – the car

If you now work with the Builder Pattern, you get to the point where you have to build complex objects. Let us now extend our example by looking at the remaining attributes of the Car class.

public class Car {
    private Engine engine;
    private List<Wheel> wheelList;
}
public class Engine {
    private int power;
    private int type;
}
public class Wheel {
    private int size;
    private int type;
    private int colour;
}

Now you can have a corresponding builder generated for each of these classes. If you stick to the basic pattern, it looks something like this for the class Wheel:

public static final class Builder {
        private int size;
        private int type;
        private int colour;
        private Builder() {}
        public Builder withSize(int size) {
            this.size = size;
            return this;
        }
        public Builder withType(int type) {
            this.type = type;
            return this;
        }
        public Builder withColour(int colour) {
            this.colour = colour;
            return this;
        }
        public Wheel build() {
            return new Wheel(this);
        }
    }

But what does it look like if you want to create an instance of the class Car? For each complex attribute of Car, we will create an instance using the builder. The resulting source code is quite extensive; at first glance, there was no reduction in volume or complexity.

public class Main {
  public static void main(String[] args) {
    Engine engine = Engine.newBuilder().withPower(100).withType(5).build();
    Wheel wheel1 = Wheel.newBuilder().withType(2).withColour(3).withSize(4).build();
    Wheel wheel2 = Wheel.newBuilder().withType(2).withColour(3).withSize(4).build();
    Wheel wheel3 = Wheel.newBuilder().withType(2).withColour(3).withSize(4).build();
    List<Wheel> wheels = new ArrayList<>();
    wheels.add(wheel1);
    wheels.add(wheel2);
    wheels.add(wheel3);
    Car car = Car.newBuilder()
                 .withEngine(engine)
                 .withWheelList(wheels)
                 .build();


    System.out.println("car = " + car);
  }
}

This source code is not very nice and by no means compact. So how can you adapt the builder pattern here so that on the one hand you have to write as little as possible by the builder himself and on the other hand you get more comfort when using it?

WheelListBuilder

Let’s take a little detour first. To be able to raise all potentials, we have to make the source text homogeneous. This strategy enables us to recognize patterns more easily. In our example, the creation of the List<Wheel> is to be outsourced to a builder, a WheelListBuilder.

public class WheelListBuilder {
    public static WheelListBuilder newBuilder(){
      return new WheelListBuilder();
    }
    private WheelListBuilder() {}
    private List<Wheel> wheelList;
    public WheelListBuilder withNewList(){
        this.wheelList = new ArrayList<>();
        return this;
    }
    public WheelListBuilder withList(List wheelList){
        this.wheelList = wheelList;
        return this;
    }
    public WheelListBuilder addWheel(Wheel wheel){
        this.wheelList.add(wheel);
        return this;
    }
    public List<Wheel> build(){
        //test if there are 4 instances....
        return this.wheelList;
    }
}

Now our example from before looks like this:

public class Main {
  public static void main(String[] args) {
    Engine engine = Engine.newBuilder().withPower(100).withType(5).build();
    Wheel wheel1 = Wheel.newBuilder().withType(2).withColour(3).withSize(4).build();
    Wheel wheel2 = Wheel.newBuilder().withType(2).withColour(3).withSize(4).build();
    Wheel wheel3 = Wheel.newBuilder().withType(2).withColour(3).withSize(4).build();
    List<Wheel> wheelList = WheelListBuilder.newBuilder()
        .withNewList()
        .addWheel(wheel1)
        .addWheel(wheel2)
        .addWheel(wheel3)
        .build();//more robust if you add tests at build()
    Car car = Car.newBuilder()
        .withEngine(engine)
        .withWheelList(wheelList)
        .build();
    System.out.println("car = " + car);
  }
}

Next, we connect the Wheel class builder and the WheelListBuilder class. The goal is to get a fluent API so that we don’t create the instances of the Wheel class individually and then use the addWheel(Wheel w) method to WheelListBuilder need to add. It should then look like this for the developer in use:

List wheels = wheelListBuilder
     .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
     .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
     .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
     .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
     .build();

So what happens here is the following: As soon as the addWheel() method is called, a new instance of the class WheelBuilder should be returned. The addWheelToList() method creates the representative of the Wheel class and adds it to the list. To do that, you have to modify the two builders involved. The addWheelToList() method is added to the WheelBuilder side. This adds the instance of the Wheel class to the WheelListBuilder and returns the instance of the WheelListBuilder class.

private WheelListBuilder wheelListBuilder;
public WheelListBuilder addWheelToList(){
  this.wheelListBuilder.addWheel(this.build());
  return this.wheelListBuilder;
}

On the side of the WheelListBuilder class, only the method addWheel()  is added.

  public Wheel.Builder addWheel() {
    Wheel.Builder builder = Wheel.newBuilder();
    builder.withWheelListBuilder(this);
    return builder;
  }

If we now transfer this to the other builders, we come to a pretty good result:

      Car car = Car.newBuilder()
          .addEngine().withPower(100).withType(5).done()
          .addWheels()
            .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
            .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
            .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
          .done()
          .build();

The NestedBuilder

So far, the builders have been modified individually by hand. However, this can be implemented generically quite easily since it is just a tree of builders.

Every builder knows his children and his father. The implementations required for this can be found in the NestedBuilder class. It is assumed here that the methods for setting attributes always begin with the prefix with. Since this seems to be the case with most generators for builders, no manual adjustment is necessary here. The method done()  sets the result of his method build()  on his father. The call is made using reflection. With this, a father knows the authority of the child. At this point, I assume that the name of the attribute is the same as the class name. We will see later how this can be achieved with different attribute names. The method withParentBuilder(..) enables the father to announce himself to his child. We have a bidirectional connection now.

public abstract class NestedBuilder<T, V> {

  public T done() {
    Class<?> parentClass = parent.getClass();
    try {
      V build = this.build();
      String methodname = "with" + build.getClass().getSimpleName();
      Method method = parentClass.getDeclaredMethod(methodname, build.getClass());
      method.invoke(parent, build);
    } catch (NoSuchMethodException 
            | IllegalAccessException 
            | InvocationTargetException e) {
      e.printStackTrace();
    }
    return parent;
  }
  public abstract V build();
  protected T parent;

  public <P extends NestedBuilder<T, V>> P withParentBuilder(T parent) {
    this.parent = parent;
    return (P) this;
  }
}

Now the specific methods for connecting with the children can be added to a father. There is no need to derive from NestedBuilder.

public class Parent {
  private KidA kidA;
  private KidB kidB;
  //snipp.....
  public static final class Builder {
    private KidA kidA;
    private KidB kidB;
    //snipp.....
    // to add manually
    private KidA.Builder builderKidA = KidA.newBuilder().withParentBuilder(this);
    private KidB.Builder builderKidB = KidB.newBuilder().withParentBuilder(this);
    public KidA.Builder addKidA() { return this.builderKidA; }
    public KidB.Builder addKidB() { return this.builderKidB; }
    //---------
    public Parent build() {
      return new Parent(this);
    }
  }
}

And with the children, it looks like this: Here, you only have to derive from NestedBuilder.

public class KidA {
  private String note;
  //snipp.....
  public static final class Builder extends NestedBuilder<Parent.Builder, KidA> {
    //snipp.....
  }
}

The use is then very compact, as shown in the previous example.

public class Main {
  public static void main(String[] args) {
    Parent build = Parent.newBuilder()
        .addKidA().withNote("A").done()
        .addKidB().withNote("B").done()
        .build();
    System.out.println("build = " + build);
  }
}

Any combination is, of course, also possible. This means that a proxy can be a father and child at the same time. Nothing stands in the way of building complex structures.

public class Main {
  public static void main(String[] args) {
    Parent build = Parent.newBuilder()
        .addKidA().withNote("A")
                  .addKidB().withNote("B").done()
        .done()
        .build();
    System.out.println("build = " + build);
  }
}

Happy Coding