Dealing with Java CVEs: Discovery, Detection, Analysis, and Resolution

Key Takeaways

  • Including a dependency vulnerability check (Software Composition Analysis or SCA) as part of a  continuous integration or continuous delivery pipeline is important to maintain an effective security posture.
  • The same vulnerability can be critical in one application and harmless in another. Humans should be “kept in the loop” here, and only the developers maintaining the application make an effective decision.
  • It is essential to prevent vulnerability alert fatigue. We should not get used to the fact that the dependency check is failing. If we do, critical vulnerability may pass unnoticed.
  • It is crucial to quickly upgrade vulnerable dependencies or suppress false positives even if we are maintaining dozens of services.
  • Developers should invest in tools that help with discovery, detection, analysis and resolution of vulnerabilities. Examples include OWASP dependency check, GitHub Dependabot, Checkmarx, Snyk and Dependency Shield.

Modern Java applications are built on top of countless open-source libraries. The libraries encapsulate common, repetitive code and allow application programmers to focus on delivering customer value. But the libraries come with a price – security vulnerabilities. A security issue in a popular library enables malicious actors to attack a wide range of targets cheaply.

Therefore, it’s crucial to have dependency vulnerability checks (a.k.a. Software Composition Analysis or SCA) as part of the CI pipeline. Unfortunately, the security world is not black and white; one vulnerability can be totally harmless in one application and a critical issue in another, so the scans always need human oversight to determine whether a report is a false positive.

This article will explore examples of vulnerabilities commonly found in standard Spring Boot projects over the last few years. This article is written from the perspective of software engineers. The focus will shift to the challenges faced when utilizing widely available tools such as the OWASP dependency check.

As software engineers are dedicated to delivering product value, they view security as one of their many responsibilities. Despite its importance, security can sometimes get in the way and be neglected because of the complexity of other tasks.

Vulnerability resolution lifecycle

A typical vulnerability lifecycle looks like this:

Discovery

A security researcher usually discovers the vulnerability. It gets reported to the impacted OSS project and, through a chain of various non-profit organizations, ends up in the NIST National Vulnerability Database (NVD). For instance, the Spring4Shell vulnerability was logged in this manner.

Detection

When a vulnerability is reported, it is necessary to detect that the application contains the vulnerable dependency. Fortunately, a plethora of tools are available that can assist with the detection.

One of the popular solutions is the OWASP dependency check – it can be used as a Gradle or Maven plugin. When executed, it compares all your application dependencies with the NIST NVD database and Sonatype OSS index. It allows you to suppress warnings and generate reports and is easy to integrate into the CI pipeline. The main downside is that it sometimes produces false positives as the NIST NVD database does not provide the data in an ideal format. Moreover, the first run takes ages as it downloads the whole vulnerability database.

Various free and commercial tools are available, such as GitHub Dependabot, Checkmarx, and Snyk. Generally, these tools function similarly, scanning all dependencies and comparing them against a database of known vulnerabilities. Commercial providers often invest in maintaining a more accurate database. As a result, commercial tools may provide fewer false positives or even negatives.

Analysis

After a vulnerability is detected, a developer must analyze the impact. As you will see in the examples below, this is often the most challenging part. The individual performing the analysis must understand the vulnerability report, the application code, and the deployment environment to see if the vulnerability can be exploited. Typically, this falls to the application programmers as they are the only ones who have all the necessary context.

Resolution

The vulnerability has to be resolved.

  1. Ideally, this is achieved by upgrading the vulnerable dependency to a fixed version.
  2.  If no fix is released yet, the application programmer may apply a workaround, such as changing a configuration, filtering an input, etc.
  3. More often than not, the vulnerability report is a false positive. Usually, the vulnerability can’t be exploited in a given environment. In such cases, the report has to be suppressed to prevent becoming accustomed to failing vulnerability reports.

Once the analysis is done, the resolution is usually straightforward but can be time-consuming, especially if there are dozens of services to patch. It’s important to simplify the resolution process as much as possible. Since this is often tedious manual work, automating it to the greatest extent possible is advisable. Tools like Dependabot or Renovate can help in this regard to some extent.

Vulnerability examples

Let’s examine some vulnerability examples and the issues that can be encountered when resolving them.

Spring4Shell (CVE-2022-22965, score 9.8)

Let’s start with a serious vulnerability – Spring Framework RCE via Data Binding on JDK 9+, a.k.a. Spring4Shell, which allows an attacker to remotely execute code just by calling HTTP endpoints.

Detection

It was easy to detect this vulnerability. Spring is quite a prominent framework; the vulnerability was present in most of its versions, and it was discussed all over the internet. Naturally, all the detection tools were able to detect it.

Analysis

In the early announcement of the vulnerability, it was stated that only applications using Spring WebMvc/Webflux deployed as WAR to a servlet container are affected. In theory, deployment with an embedded servlet container should be safe. Unfortunately, the announcement lacked the vulnerability details, making it difficult to confirm whether this was indeed the case. However, this vulnerability was highly serious, so it should have been mitigated promptly.

Resolution

The fix was released in a matter of hours, so the best way was to wait for the fix and upgrade. Tools like Dependabot or Renovate can help to do that in all your services.

If there was a desire to resolve the vulnerability sooner, a workaround was available. But it meant applying an obscure configuration without a clear understanding of what it did. The decision to manually apply it across all services or wait for the fix could have been a challenging one to make.

HttpInvoker RCE (CVE-2016-1000027, score 9.8)

Let’s continue to focus on Spring for a moment. This vulnerability has the same criticality as Spring4Shell 9.8. But one might notice the date is 2016 and wonder why it hasn’t been fixed yet or why it lacks a fancy name. The reason lies in its location within the HttpInvoker component, used for the RPC communication style. It was popular in the 2000s but is seldom used nowadays. To make it even more confusing, the vulnerability was published in 2020, four years after it was initially reported due to some administrative reasons.

Detection

This issue was reported by OWASP dependency check and other tools. As it did not affect many, it did not make the headlines.

Analysis

Reading the NIST CVE detail doesn’t reveal much:

Pivotal Spring Framework through 5.3.16 suffers from a potential remote code execution (RCE) issue if used for Java deserialization of untrusted data. Depending on how the library is implemented within a product, this issue may or [may] not occur, and authentication may be required.

This sounds pretty serious, prompting immediate attention and a search through the link to find more details. However, the concern turns out to be a false alarm, as it only applies if HttpInvokerServiceExporter is used.

Resolution

No fixed version of a library was released, as Pivotal did not consider it a bug. It was a feature of an obsolete code that was supposed to be used only for internal communication. The whole functionality was dropped altogether in Spring 6, a few years later.

The only action that to take is to suppress the warning. Using the free OWASP dependency check, this process can be quite time-consuming if it has to be done manually for each service.

There are several ways to simplify the flow. One is to expose and use a shared suppression file in all your projects by specifying its URL. Lastly, you can employ a simple service like Dependency Shield to streamline the whole suppression flow. The important point is that a process is needed to simplify the suppression, as most of the reports received are likely false positives.

SnakeYAML RCE (CVE-2022-1471, score 9.8)

Another critical vulnerability has emerged, this time in the SnakeYAML parsing library. Once again, it involves remote code execution, with a score of 9.8. However, it was only applicable if the SnakeYAML Constructor class had been used to parse a YAML provided by an attacker.

Detection

It was detected by vulnerability scanning tools. SnakeYAML is used by Spring to parse YAML configuration files, so it’s quite widespread.

Analysis

Is the application parsing YAML that could be provided by an attacker, for example, on a REST API? Is the unsafe Constructor class being used? If so, the system is vulnerable. The system is safe if it is simply used to parse Spring configuration files. An individual who understands the code and its usage must make the decision. The situation could either be critical, requiring immediate attention and correction, or it could be safe and therefore ignored.

Resolution

The issue was quickly fixed. What made it tricky was that SnakeYAML was not a direct dependency; it’s introduced transitively by Spring, which made it harder to upgrade. If you want to upgrade SnakeYAML, you may do it in several ways.

  1. If using the Spring Boot dependency management plugin with Spring Boot BOM,
    • a.    the snakeyaml.version variable can be overridden.
    • b.    the dependency management declaration can be overridden.
  2. If not using dependency management, SnakeYAML must be added as a direct dependency to the project, and the version must be overridden.

When combined with complex multi-project builds, it’s almost impossible for tools to upgrade the version automatically. Both Dependabot and Renovate are not able to do that. Even a commercial tool like Snyk is failing with “could not apply the upgrade, dependency is managed externally.”

And, of course, once the version is overridden, it is essential to remember to remove the override once the new version is updated in Spring. In our case, it’s better to temporarily suppress the warning until the new version is used in Spring.

Misidentified Avro vulnerability

Vulnerability CVE-2021-43045 is a bug in .NET versions of the Avro library, so it’s unlikely to affect a Java project. How, then, is it reported? Unfortunately, the NIST report contains cpe:2.3:a:apache:avro:*:*:*:*:*:*:*:* identifier. No wonder the tools mistakenly identify org.apache.avro/[email protected] as vulnerable, even though it’s from a completely different ecosystem.

Resolution: Suppress

Summary

Let’s look back at the different stages of the vulnerability resolution and how to streamline it as much as possible so the reports do not block the engineers for too long.

Detection

The most important part of detection is to avoid getting used to failing dependency checks. Ideally, the build should fail if there is a vulnerable dependency detected. To be able to enable that, the resolution needs to be as painless and as fast as possible. No one wants to encounter a broken pipeline due to a false positive.
 
Since the OWASP dependency check primarily uses the NIST NVD database, it sometimes struggles with false positives. However, as has been observed, false positives are inevitable, as the analysis is only occasionally straightforward.

Analysis

This is the hard part and actually, the one when tooling can’t help us much. Consider the SnakeYAML remote code execution vulnerability as an example. For it to be exploitable, the library would have to be used unsafely, such as parsing data provided by an attacker. Regrettably, no tool is likely to reliably detect whether an application and all its libraries contain vulnerable code. So, this part will always need some human intervention.

Resolution

Upgrading the library to a fixed version is relatively straightforward for direct dependencies. Tools like Dependabot and Renovate can help in the process. However, the tools fail if the vulnerable dependency is introduced transitively or through dependency management. Manually overriding the dependency may be an acceptable solution for a single project. In cases where multiple services are being maintained, we should introduce centrally managed dependency management to streamline the process.

Most reports are false positives, so it’s crucial to have an easy way to suppress the warning. When using OWASP dependency check, either try a shared suppression file or a tool like Dependency Shield that helps with this task.

It often makes sense to suppress the report temporarily. Either to unblock the pipeline until somebody has time to analyze the report properly or until the transitive dependency is updated in the project that introduced it.

Building Kafka Event-Driven Applications with KafkaFlow

Key Takeaways

  • KafkaFlow is an open-source project that streamlines Kafka-based event-driven applications, simplifying the development of Kafka consumers and producers.
  • The .NET framework offers an extensive range of features, including middleware, message handlers, type-based deserialization, concurrency control, batch processing, etc.
  • By utilizing middlewares, developers can encapsulate the logic for processing messages, which leads to better separation of concerns and maintainable code.
  • The project can be extended, creating the possibility of customization and the growth of an ecosystem of add-ons.
  • Developers benefit from KafkaFlow by being able to focus on what matters, spending more time on the business logic rather than investing in low-level concerns.

KafkaFlow is an open-source framework by FARFETCH. It helps .NET developers working with Apache Kafka to create event-driven applications. KafkaFlow lets developers easily set up “Consumers” and “Producers.” The simplicity makes it an attractive framework for businesses seeking efficiency and robustness in their applications.

In this article, we will explore what KafkaFlow has to offer. If you build Apache Kafka Consumers and Producers using .NET, this article will provide a glance at how KafkaFlow can simplify your life.

Why Should You Care About It?

KafkaFlow provides an abstraction layer over the Confluent .NET Kafka client. It does so while making it easier to use, maintain, and test Kafka consumers and producers.

Imagine you need to build a Client Catalog for marketing initiatives. You will need a service to consume messages that capture new Clients. Once you start laying out your required service, you notice that existing services are not consistent in how they consume messages.

It’s common to see teams struggling and often solving simple problems such as graceful shutdowns. You’ve figured out that you have four different implementations of a JSON serializer across the organization, just to name one of the challenges.

Adopting a framework like KafkaFlow simplifies the process and can speed up the development cycle. KafkaFlow has a set of features designed to enhance the developer experience:

  1. Middlewares: KafkaFlow allows developers to create middlewares to process messages, enabling more control and customization of the Kafka consumer/producer pipeline.
  2. Handlers: Introduces the concept of message handlers, allowing developers to forward message processing from a topic to a message-type dedicated handler.
  3. Deserialization Algorithms: Offers a set of Serialization and Deserialization algorithms out-of-the-box.
  4. Multi-threaded Consumers: Provides multi-threading with message order guaranteed, helping to ensure optimal use of system resources.
  5. Administration API and Dashboard: Provides API and Dashboards to manage Consumers and Consumer groups, with operations such as pausing, resuming, or rewinding offsets, all at runtime.
  6. Consumer Throttling: Provides an easy way to bring priorities to topic consumption.

Let’s explore them so you can see the potential to address a problem like this.

KafkaFlow Producers: Simplified Message Production

Let’s start with message producers.

Producing a Message to Kafka is not rocket science. Even then, KafkaFlow provides a higher-level abstraction over the producer interface from Confluent’s .NET Kafka client, simplifying the code and increasing maintainability.

Here’s an example of how to send a message with a KafkaFlow producer:

await _producers["my-topic-events"]
    .ProduceAsync("my-topic", message.Id.ToString(), message);

This way, you can produce messages to Kafka without dealing directly with serialization or other complexities of the underlying Kafka client.

Not only that, but defining and managing Producers is pleasantly done through a Fluent Interface on your service configuration.

services.AddKafka(kafka => kafka
    .AddCluster(cluster => cluster
        .WithBrokers(new[] { "host:9092" })
        .AddProducer(
            "product-events",
            producer =>
                producer
            ...
        )
    )
);

Producers tend to be simple, but there are some common concerns to address, like compression or serialization. Let’s explore that.

Custom Serialization/Deserialization in KafkaFlow

One of the attractive features of Apache Kafka is being agnostic of data formats. However, that transfers the responsibility to producers and consumers. Without a thoughtful approach, it may lead to many ways to achieve the same result across the system. That makes serialization an obvious use case to be handled by a client framework.

KafkaFlow has serializers available for JSON, Protobuf, and even Avro. Those can be used simply by adding them to the middleware configuration.

.AddProducer(producer => producer
       ...
       .AddMiddlewares(middlewares => middleware
           ...
           .AddSerializer()
       )
)

The list is not restricted to those three due to its ability to use custom serializers/deserializers for messages. While Confluent’s .NET Kafka client already supports custom serialization/deserialization, KafkaFlow simplifies the process by providing a more elegant way to handle it.

As an example, to use a custom serializer, you would do something like this:

public class MySerializer : ISerializer
{
       public Task SerializeAsync(object message, Stream output, ISerializerContext context)
       {
             // Serialization logic here
       }

       public async Task DeserializeAsync(Stream input, Type type, ISerializerContext context)
       {
             // Deserialization logic here
       }
}

// Register the custom serializer when setting up the Kafka consumer/producer

.AddProducer(producer => producer
       ...
       .AddMiddlewares(middlewares => middleware
       	  ...
       	  .AddSerializer()
       )
)

Message Handling in KafkaFlow

Consumers bring a ton of questions and possibilities. The first one is “How do you handle a message?”

Let’s start with the simplest way. With the advent of libraries like MediatR that popularized the CQRS and the Meditor Patterns, .NET developers got used to decoupling message handlers from the request/message receiver. KafkaFlow brings that same principle to Kafka Consumers.

KafkaFlow message handlers allow developers to define specific logic to process messages from a Kafka topic. KafkaFlow’s message handler structure is designed for better separation of concerns and cleaner, more maintainable code.

Here’s an example of a message handler:

public class MyMessageHandler : IMessageHandler
{
    public Task Handle(IMessageContext context, MyMessageType message)
    {
        // Message handling logic here.
    }
}

This handler can be registered in the consumer configuration:

.AddConsumer(consumer => consumer
...
       .AddMiddlewares(middlewares => middlewares
           ...
             .AddTypedHandlers(handlers => handlers
                     .AddHandler()
              )
       )
)

With this approach, it’s easy to separate Consumers from Handlers, simplifying maintainability and testability.

This may look like unneeded complexity if you have a microservice handling one topic with only one message type. In that case, you can take advantage of middlewares.

Middleware in KafkaFlow

KafkaFlow is middleware-oriented. Maybe you noticed on the Message Handlers snippets a reference to “Middlewares.” So, you may be asking yourself what a Middleware is.

Middlewares are what make Typed Handlers possible. Messages are delivered to a middleware pipeline that will be invoked in sequence. You might be familiar with this concept if you have used MediatR pipelines. Also, Middlewares can be used to apply a series of transformations. In other words, a given Middleware can transform the incoming message to the following Middleware.

A Middleware in KafkaFlow encapsulates the logic for processing messages. The pipeline is extensible, allowing developers to add behavior to the message-processing pipeline.

Here’s an example of a middleware:

public class MyMiddleware : IMessageMiddleware
{
    public async Task Invoke(IMessageContext context, MiddlewareDelegate next)
    {
         // Pre-processing logic here.          
        await next(context);          
         // Post-processing logic here.     
    }
}

To use this middleware, it can be registered in the consumer configuration:

.AddConsumer(consumer => consumer
       ...
       .AddMiddlewares(middlewares => middlewares
             ...
             .Add()
         )
)   

This way, developers can plug-in custom logic into the message processing pipeline, providing flexibility and control.

Typed Handlers are a form of Middleware. So, you can even handle a message without a Typed Handler, implementing your middleware, or you can take advantage of Middlewares to build a Message pipeline that performs validations, enrichment, etc., before handling that message.

Handling Concurrency in KafkaFlow

Once you start thinking about infrastructure efficiency, you will notice that many Kafka Consumers are underutilized. The most common implementation is single-threaded, which caps resource utilization. So, when you need to scale, you do it horizontally to keep the desired throughput.

KafkaFlow brings another option to achieve infrastructure efficiency. KafkaFlow gives developers control over how many messages can be processed concurrently by a single consumer. It uses the concept of Workers that can work together consuming a topic.
This functionality allows you to optimize your Kafka consumer to better match your system’s capabilities.

Here’s an example of how to set the number of concurrent workers for a consumer:

.AddConsumer(consumer => consumer
.Topic("topic-name")
       .WithGroupId("sample-group")
       .WithBufferSize(100)
       .WithWorkersCount(10) // Set the number of workers.
       .AddMiddlewares(middlewares => middlewares
       	...
      	)
)

KafkaFlow guarantees order even with concurrent workers.

Batch Processing

With scale, you will face the tradeoff between latency and throughput. To handle that tradeoff, KafkaFlow has an important feature called “Batch Consuming.” This feature addresses the need for efficiency and performance in consuming and processing messages from Kafka in a batch-wise manner. It plays an important role in use cases where a group of messages needs to be processed together rather than individually.

What Is Batch Consuming?

Batch consuming is an approach where instead of processing messages atomically as they come in, the system groups several messages together and processes them all at once. This method is more efficient for dealing with large amounts of data, particularly if messages are independent of each other. Performing operations as a batch will lead to an increase in overall performance.

KafkaFlow’s Approach to Batch Consuming

KafkaFlow takes advantage of the system of Middlewares to provide batch processing. The Batch Processing Middleware lets you group messages according to batch size or timespan. Once one of those conditions is reached, the Middleware will forward the group of messages to the next middleware.

services.AddKafka(kafka => kafka
    .AddCluster(cluster => cluster
        .WithBrokers(new[] { "host:9092" })
        .AddConsumer(
            consumerBuilder => consumerBuilder
            ...
            .AddMiddlewares(
                middlewares => middlewares
                    ...
                    .BatchConsume(100, TimeSpan.FromSeconds(10))
                    .Add()
            )
        )
    )
);

The Impact of Batch Consuming on Performance

With batch processing, developers can achieve higher throughput in their Kafka-based applications. It allows for faster processing as the overhead associated with initiating and finalizing each processing task is significantly reduced. This leads to an overall increase in system performance.

Also, this approach reduces network I/O operations as data is fetched in larger chunks, which can further improve processing speed, especially in systems where network latency is a concern.

Consumer Administration with KafkaFlow

KafkaFlow also simplifies administrative tasks related to managing Kafka consumers. You can start, stop, pause consumers, rewind offsets, and much more with KafkaFlow’s administrative API.

The Administration API can be used throughout a programming interface, REST API, or a Dashboard UI.

[Click on the image to view full-size]

KafkaFlow administration Dashboard

Consumer Throttling

Often, underlying technologies may not be able to deal with high-load periods in the same way as Kafka Consumers. That can bring stability problems. That is where throttling comes in.

Consumer Throttling is an approach to managing the consumption of messages, enabling applications to dynamically fine-tune the rate at which they consume messages based on metrics.

Prioritization

Imagine you’re running an application where you want to segregate atomic and bulk actions into different consumers and topics. You may prefer to prioritize the processing of atomic actions over bulk actions. Traditionally, managing this differentiation could be challenging, given the potential discrepancies in the rate of message production.

Consumer Throttling is valuable in such instances, allowing you to monitor the consumer lag of the consumer responsible for atomic actions. Based on this metric, you can apply throttling to the consumer handling the bulk actions, ensuring that atomic actions are processed as a priority.

The result? An efficient, flexible, and optimized consumption process.

Adding throttling to a consumer is straightforward with a KafkaFlow fluent interface. Here’s a simple example:

.AddConsumer(
    consumer => consumer
        .Topic("bulk-topic")
        .WithName("bulkConsumer")
        .AddMiddlewares(
            middlewares => middlewares
                .ThrottleConsumer(
                    t => t
                        .ByOtherConsumersLag("singleConsumer")
                        .WithInterval(TimeSpan.FromSeconds(5))
                        .AddAction(a => a.AboveThreshold(10).ApplyDelay(100))
                        .AddAction(a => a.AboveThreshold(100).ApplyDelay(1_000))
                        .AddAction(a => a.AboveThreshold(1_000).ApplyDelay(10_000)))
                .AddSerializer()
        )
)

KafkaFlow: Looking Toward the Future

As of now, KafkaFlow provides a robust, developer-friendly abstraction over Kafka that simplifies building real-time data processing applications with .NET. However, like any active open-source project, it’s continually evolving and improving.

Given the project’s current trajectory, we might anticipate several developments. For instance, KafkaFlow could further enhance its middleware system, providing even more control and flexibility over message processing. We might also see more extensive administrative APIs, providing developers with even greater control over their Kafka clusters.

Being extensible by design, we can expect the KafkaFlow community to grow, leading to more contributions, innovative features, extensions, and support. As more developers and organizations adopt KafkaFlow, we’re likely to see an increase in learning resources, tutorials, case studies, and other community-generated content that can help new users get started and existing users get more from the library.

Conclusion

KafkaFlow is a handy and developer-friendly tool that simplifies work with Kafka in .NET. It shines in the area of developer experience and usability. The framework design lends itself well to clean, readable code. With a clear separation of concerns through middlewares and message handlers, as well as abstractions over complex problems when building applications on top of Apache Kafka, KafkaFlow helps to keep your codebase manageable and understandable.

Furthermore, the community around KafkaFlow is growing. If you’re using Kafka and looking to improve productivity and reliability, KafkaFlow is certainly worth considering.

Streamlining Code with Unnamed Patterns/Variables: A Comparative Study of Java, Kotlin, and Scala

Key Takeaways

  • Java’s JEP 443: Enhances code readability by allowing the omission of unnecessary components in pattern matching and unused variables.
  • Kotlin’s unused variables: Simplifies code by denoting unused parameters in functions, lambdas, or destructuring declarations.
  • Scala’s unused variables: Used as a wildcard to ignore unused variables and conversions, improving code conciseness.
  • Underscore as Syntactic Sugar: A common feature in many languages, including Java, Kotlin, and Scala, that simplifies code.
  • Enhanced Code Readability and Maintainability: The underscore character improves code readability and maintainability.
  • Future Language Evolution: Expect further enhancements and innovative uses of the underscore as languages evolve.

In the world of programming, the underscore (`_`) is a character with a wide range of uses. It’s often referred to as syntactic sugar, as it simplifies the code and makes it more concise.

This article will explore the use of underscores in three popular programming languages: Java, Kotlin, and Scala.

Java: Unnamed Patterns and Variables with JEP 443

Java, the ever-evolving language, has taken another significant step towards enhancing its code readability and maintainability with the introduction of JEP 443. This proposal, titled “Unnamed Patterns and Variables (Preview),” has been completed from the targeted status for JDK 21.

The JEP aims to enhance the language with unnamed patterns, which match a record component without stating the component’s name or type, and unnamed variables, which you can but not use.

Both of these are denoted by the underscore character, as in r instanceof _(int x, int y) and r instanceof _.

Unnamed Patterns

Unnamed patterns are designed to streamline data processing, particularly when working with record classes. They allow developers to omit the type and name of a record component in pattern matching, which can significantly improve code readability.

Consider the following code snippet:

if (r instanceof ColoredPoint(Point p, Color c)) {
    // ...
}

If the Color c component is not needed in the if block, it can be laborious and unclear to include it in the pattern. With JEP 443, developers can simply omit unnecessary components, resulting in cleaner, more readable code:

if (r instanceof ColoredPoint(Point p, _)) {
    // ...
}

This feature is particularly useful in nested pattern-matching scenarios where only some components of a record class are required. For example, consider a record class ColoredPoint that contains a Point and a Color. If you only need the x coordinate of the Point, you can use an unnamed pattern to omit the y and Color components:

if (r instanceof ColoredPoint(Point(int x, _), _)) {
    // ...
}

Unnamed Variables

Unnamed variables are useful when a variable must be declared, but its value is not used. This is common in loops, try-with-resources statements, catch blocks, and lambda expressions.

For instance, consider the following loop:

for (Order order : orders) {
    if (total < limit) total++;
}

In this case, the order variable is not used within the loop. With JEP 443, developers can replace the unused variable with an underscore, making the code more concise and clear:

for (_ : orders) {
    if (total < limit) total++;
}

Unnamed variables can also be beneficial in switch statements where the same action is executed for multiple cases, and the variables are not used. For example:

switch (b) {
    case Box(RedBall _), Box(BlueBall _) -> processBox(b);
    case Box(GreenBall _) -> stopProcessing();
    case Box(_) -> pickAnotherBox();
}

In this example, the first two cases use unnamed pattern variables because their right-hand sides do not use the box’s component. The third case uses the unnamed pattern to match a box with a null component.

Enabling the Preview Feature

Unnamed patterns and variables are a preview feature, disabled by default. To use it, developers must enable the preview feature to compile this code, as shown in the following command:

javac --release 21 --enable-preview Main.java

The same flag is also required to run the program:

java --enable-preview Main

However, one can directly run this using the source code launcher. In that case, the command line would be:

java --source 21 --enable-preview Main.java

The jshell option is also available but requires enabling the preview feature as well:

jshell --enable-preview

Kotlin: Underscore for Unused Parameters

In Kotlin, the underscore character (_) is used to denote unused parameters in a function, lambda, or destructuring declaration. This feature allows developers to omit names for such parameters, leading to cleaner and more concise code.

In Kotlin, developers can place an underscore instead of its name if the lambda parameter is unused. This is particularly useful when working with functions that require a lambda with multiple parameters but only some of the parameters are needed.

Consider the following Kotlin code snippet:

mapOf(1 to "one", 2 to "two", 3 to "three")
   .forEach { (_, value) -> println("$value!") }

In this example, the forEach function requires a lambda that takes two parameters: a key and a value. However, we’re only interested in the value, so we replace the key parameter with an underscore.

Let’s consider another code snippet:

var name: String by Delegates.observable("no name") {
    kProperty, oldValue, newValue -> println("$oldValue")
}

In this instance, if the kProperty and newValue parameters are not used within the lambda, including them can be laborious and unclear. With the underscore feature, developers can simply replace the unused parameters with underscores:

var name: String by Delegates.observable("no name") {
    _, oldValue, _ -> println("$oldValue")
}

This feature is also useful in destructuring declarations where you want to skip some of the components:

val (w, x, y, z) = listOf(1, 2, 3, 4)
print(x + z) // 'w' and 'y' remain unused

With the underscore feature, developers can replace the unused components with underscores:

val (_, x, _, z) = listOf(1, 2, 3, 4)
print(x + z)

This feature is not unique to Kotlin. Other languages like Haskell use the underscore character as a wildcard in pattern matching. For C#, `_` in lambdas is just an idiom without special treatment in the language. The same semantic may be applied in future versions of Java.

Scala: The Versatility of Underscore

In Scala, the underscore (`_`) is a versatile character with a wide range of uses. However, this can sometimes lead to confusion and increase the learning curve for new Scala developers. In this section, we’ll explore the different and most common usages of underscores in Scala.

Pattern Matching and Wildcards

The underscore is widely used as a wildcard and in matching unknown patterns. This is perhaps the first usage of underscore that Scala developers would encounter.

Module Import

We use underscore when importing packages to indicate that all or some members of the module should be imported:

// imports all the members of the package junit. (equivalent to wildcard import in java using *)
import org.junit._

// imports all the members of junit except Before.
import org.junit.{Before => _, _}

// imports all the members of junit but renames Before to B4.
import org.junit.{Before => B4, _}

Existential Types

The underscore is also used as a wildcard to match all types in type creators such as List, Array, Seq, Option, or Vector.

// Using underscore in List
val list: List[_] = List(1, "two", true)
println(list)

// Using underscore in Array
val array: Array[_] = Array(1, "two", true)
println(array.mkString("Array(", ", ", ")"))

// Using underscore in Seq
val seq: Seq[_] = Seq(1, "two", true)
println(seq)

// Using underscore in Option
val opt: Option[_] = Some("Hello")
println(opt)

// Using underscore in Vector
val vector: Vector[_] = Vector(1, "two", true)
println(vector)

With `_`, we allowed all types of elements in the inner list.

Matching

Using the match keyword, developers can use the underscore to catch all possible cases not handled by any of the defined cases. For example, given an item price, the decision to buy or sell the item is made based on certain special prices. If the price is 130, the item is to buy, but if it’s 150, it is to sell. For any other price outside these, approval needs to be obtained:

def itemTransaction(price: Double): String = {
 price match {
   case 130 => "Buy"
   case 150 => "Sell"

   // if price is not any of 130 and 150, this case is executed
   case _ => "Need approval"
 }
}

println(itemTransaction(130)) // Buy
println(itemTransaction(150)) // Sell
println(itemTransaction(70))  // Need approval
println(itemTransaction(400)) // Need approval

Ignoring Things

The underscore can ignore variables and types not used anywhere in the code.

Ignored Parameter

For example, in function execution, developers can use the underscore to hide unused parameters:

val ints = (1 to 4).map(_ => "Hello")
println(ints) // Vector(Hello, Hello, Hello, Hello)

Developers can also use the underscore to access nested collections:

val books = Seq(("Moby Dick", "Melville", 1851), ("The Great Gatsby", "Fitzgerald", 1925), ("1984", "Orwell", 1949), ("Brave New World", "Huxley", 1932))

val recentBooks = books
 .filter(_._3 > 1900)  // filter in only books published after 1900
 .filter(_._2.startsWith("F"))  // filter in only books whose author's name starts with 'F'
 .map(_._1)  
// return only the first element of the tuple; the book title

println(recentBooks) // List(The Great Gatsby)

In this example, the underscore is used to refer to the elements of the tuples in the list. The filter function selects only the books that satisfy the given conditions, and then the map function transforms the tuples to just their first element (book title). The result is a sequence with book titles that meet the criteria.

Ignored Variable

When a developer encounters details that aren’t necessary or relevant, they can utilize the underscore to ignore them.

For example, a developer wants only the first element in a split string:

val text = "a,b"
val Array(a, _) = text.split(",")
println(a)

The same principle applies if a developer only wants to consider the second element in a construct.

val Array(_, b) = text.split(",")
println(b)

The principle can indeed be extended to more than two entries. For instance, consider the following example:

val text = "a,b,c,d,e"
val Array(a, _*) = text.split(",")
println(a)

In this example, a developer splits the text into an array of elements. However, they are only interested in the first element, 'a'. The underscore with an asterisk  (_*) ignores the rest of the entries in the array, focusing only on the required element.

To ignore the rest of the entries after the first, we use the underscore together with `*`.

The underscore can also be used to ignore randomly:

val text = "a,b,c,d,e"
val Array(a, b, _, d, e) = text.split(",")
println(a)
println(b)
println(d)
println(e)

Variable Initialization to Its Default Value

When the initial value of a variable is not necessary, you can use the underscore as default:

var x: String = _
x = "real value"
println(x) // real value

However, this doesn’t work for local variables; local variables must be initialized.

Conversions

In several ways, you can use the underscore in conversions.

Function Reassignment (Eta expansion)

With the underscore, a method can be converted to a function. This can be useful to pass around a function as a first-class value.

def greet(prefix: String, name: String): String = s"$prefix $name"

// Eta expansion to turn greet into a function
val greeting = greet _

println(greeting("Hello", "John"))

Variable Argument Sequence

A sequence can be converted to variable arguments using `seqName: _*` (a special instance of type ascription).

def multiply(numbers: Int*): Int = {
 numbers.reduce(_ * _)
}

val factors = Seq(2, 3, 4)
val product = multiply(factors: _*)
// Convert the Seq factors to varargs using factors: _*

println(product) // Should print: 24

Partially-Applied Function

By providing only a portion of the required arguments in a function and leaving the remainder to be passed later, a developer can create what’s known as a partially-applied function. The underscore substitutes for the parameters that have not yet been provided.

def sum(x: Int, y: Int): Int = x + y
val sumToTen = sum(10, _: Int)
val sumFiveAndTen = sumToTen(5)

println(sumFiveAndTen, 15)

The use of underscores in a partially-applied function can also be grouped as ignoring things. A developer can ignore entire groups of parameters in functions with multiple parameter groups, creating a special kind of partially-applied function:

def bar(x: Int, y: Int)(z: String, a: String)(b: Float, c: Float): Int = x
val foo = bar(1, 2) _

println(foo("Some string", "Another string")(3 / 5, 6 / 5), 1)

Assignment Operators (Setters overriding)

Overriding the default setter can be considered a kind of conversion using the underscore:

class User {
 private var pass = ""
 def password = pass
 def password_=(str: String): Unit = {
   require(str.nonEmpty, "Password cannot be empty")
   require(str.length >= 6, "Password length must be at least 6 characters")
   pass = str
 }
}

val user = new User
user.password = "Secr3tC0de"
println(user.password) // should print: "Secr3tC0de"

try {
 user.password = "123" // will fail because it's less than 6 characters
 println("Password should be at least 6 characters")
} catch {
 case _: IllegalArgumentException => println("Invalid password")
}

Higher-Kinded Type

A Higher-Kinded type is one that abstracts over some type that, in turn, abstracts over another type. In this way, Scala can generalize across type constructors. It’s quite similar to the existential type. It can be defined higher-kinded types using the underscore:

trait Wrapper[F[_]] {
 def wrap[A](value: A): F[A]
}

object OptionWrapper extends Wrapper[Option] {
 override def wrap[A](value: A): Option[A] = Option(value)
}

val wrappedInt = OptionWrapper.wrap(5)
println(wrappedInt)

val wrappedString = OptionWrapper.wrap("Hello")
println(wrappedString)

In the above example, Wrapper is a trait with a higher-kinded type parameter F[_]. It provides a method wrap that wraps a value into the given type. OptionWrapper is an object extending this trait for the Option type. The underscore in F[_] represents any type, making Wrapper generic across all types of Option.

These are some examples of Scala being a powerful tool that can be used in various ways to simplify and improve the readability of your code. It’s a feature that aligns well with Scala’s philosophy of being a concise and expressive language that promotes readable and maintainable code.

Conclusion

The introduction of unnamed patterns and variables in Java through JEP 443 marks a significant milestone in the language’s evolution. This feature, which allows developers to streamline their code by omitting unnecessary components and replacing unused variables, brings Java closer to the expressiveness and versatility of languages like Kotlin and Scala.

However, it’s important to note that while this is a substantial step forward, Java’s journey in this area is still incomplete. Languages like Kotlin and Scala have long embraced similar concepts, using them in various ways to enhance code readability, maintainability, and conciseness. These languages have demonstrated the power of such concepts in making code more efficient and easier to understand.

In comparison, Java’s current use of unnamed patterns and variables, although beneficial, is still somewhat limited. The potential for Java to further leverage these concepts is vast. Future updates to the language could incorporate more advanced uses of unnamed patterns and variables, drawing inspiration from how these concepts are utilized in languages like Kotlin and Scala.

Nonetheless, adopting unnamed patterns and variables in Java is a significant step towards enhancing the language’s expressiveness and readability. As Java continues to evolve and grow, we expect to see further innovative uses of these concepts, leading to more efficient and maintainable code. The journey is ongoing, and it’s an exciting time to be a part of the Java community.

Happy coding!

Leveraging Eclipse JNoSQL 1.0.0: Quarkus Integration and Building a Pet-Friendly REST API

Key Takeaways

  • Eclipse JNoSQL leverages the Jakarta EE standard specifications, specifically Jakarta NoSQL and Jakarta Data, to ensure compatibility with various NoSQL database vendors and promote interoperability.
  • Eclipse JNoSQL seamlessly integrates with the Quarkus framework, enabling developers to build cloud-native applications with the benefits of both frameworks, such as rapid development, scalability, and resilience.
  • With Eclipse JNoSQL, developers can simplify the integration process, communicate seamlessly with diverse NoSQL databases, and future-proof their applications by easily adapting to changing database requirements.
  • By embracing Eclipse JNoSQL, developers can unlock the power of NoSQL databases while maintaining a familiar programming syntax, enabling efficient and effective data management in modern application development.
  • Eclipse JNoSQL 1.0.0 marks a significant milestone in the evolution of NoSQL database integration, providing developers with comprehensive tools and features to streamline their data management processes.
  • The release of Eclipse JNoSQL empowers developers to leverage the benefits of NoSQL databases, including scalability, flexibility, and performance, while ensuring compatibility and ease of use through standardized specifications.

In today’s data-driven world, the ability to seamlessly integrate and manage data from diverse sources is crucial for the success of modern applications. Eclipse JNoSQL, with its latest release of version 1.0.0, presents a comprehensive solution that simplifies the integration of NoSQL databases. This article explores the exciting new features and enhancements introduced in Eclipse JNoSQL 1.0.0, highlighting its significance in empowering developers to efficiently harness the power of NoSQL databases. From advanced querying capabilities to seamless compatibility with the Quarkus framework, Eclipse JNoSQL opens up new possibilities for streamlined and future-proof data management.

Polyglot persistence refers to utilizing multiple database technologies to store different data types within a single application. It recognizes that other data models and storage technologies are better suited for specific use cases. In modern enterprise applications, polyglot persistence is crucial for several reasons.

Firstly, it allows enterprises to leverage the strengths of various database technologies, including NoSQL databases, to efficiently handle different data requirements. NoSQL databases excel at managing unstructured, semi-structured, and highly scalable data, making them ideal for scenarios like real-time analytics, content management systems, or IoT applications.

By adopting polyglot persistence, enterprises can select the most suitable database technology for each data model, optimizing performance, scalability, and flexibility. For example, a social media platform may store user profiles and relationships in a graph database while utilizing a document database for managing user-generated content.

Eclipse JNoSQL, an open-source framework, simplifies the integration of NoSQL databases within Jakarta EE applications. It provides a unified API and toolset, abstracting the complexities of working with different NoSQL databases and facilitating seamless development and maintenance.

Eclipse JNoSQL is a compatible implementation of Jakarta NoSQL, a specification defining a standard API for interacting with various NoSQL databases in a Jakarta EE environment. By embracing Jakarta NoSQL, developers can leverage Eclipse JNoSQL to seamlessly integrate different NoSQL databases into their Jakarta EE applications, ensuring vendor independence and flexibility.

Why Eclipse JNoSQL?

Eclipse JNoSQL serves as a Java solution for seamless integration between Java and NoSQL databases, specifically catering to the needs of enterprise applications. It achieves this by providing a unified API and utilizing the specifications based on four different NoSQL database types: key-value, column family, document, and graph.

[Click on the image to view full-size]

Using Eclipse JNoSQL, developers can leverage the same mapping annotations, such as @Entity, @Id, and @Column, regardless of the underlying NoSQL database. This approach allows developers to explore the benefits of different NoSQL databases without the burden of learning multiple APIs. It reduces the cognitive load and will enable developers to focus more on business logic while taking full advantage of the capabilities offered by the NoSQL database.

The extensibility of the Eclipse JNoSQL API is another critical advantage. It allows developers to work with specific behaviors of different NoSQL databases. For example, developers can utilize the Cassandra Query Language (CQL) through the same API if working with Cassandra.

The use of Eclipse JNoSQL simplifies the transition between different NoSQL databases. Without learning other classes and methods, developers can utilize the same API to work with multiple databases, such as MongoDB and ArangoDB. This approach enhances developer productivity and reduces the learning curve of integrating various NoSQL databases.

[Click on the image to view full-size]

While the Jakarta Persistence specification is commonly used for relational databases, it is unsuitable for NoSQL databases due to the fundamental differences in behavior and data models. Eclipse JNoSQL acknowledges these differences and provides a dedicated API explicitly designed for NoSQL databases, enabling developers to effectively leverage the unique capabilities of each NoSQL database.

Additionally, when working with the graph database implementation in Eclipse JNoSQL, it utilizes Apache TinkerPop, a standard interface for interacting with graph databases. By leveraging Apache TinkerPop, Eclipse JNoSQL ensures compatibility with various graph database vendors, allowing developers to work seamlessly with different graph databases using a consistent API. This standardization simplifies graph database integration, promotes interoperability, and empowers developers to harness the full potential of graph data in enterprise applications.

Eclipse JNoSQL simplifies Java and NoSQL database integration for enterprise applications. It provides a unified API, allowing developers to focus on business logic while seamlessly working with different NoSQL databases. Developers can explore the benefits of NoSQL databases without learning multiple APIs, thereby improving development efficiency and reducing the cognitive load associated with integrating diverse data sources.

Eclipse JNoSQL is an advanced Java framework that facilitates seamless integration between Java applications and various persistence layers, explicitly focusing on NoSQL databases. It supports two key specifications under the Jakarta EE umbrella: Jakarta Data and Jakarta NoSQL.

[Click on the image to view full-size]

  • Jakarta Data simplifies the integration process between Java applications and different persistence layers. It provides a unified repository interface that allows developers to work with multiple persistence layers using a single interface. This feature eliminates the need to learn and adapt to different APIs for each persistence layer, streamlining the development process. Additionally, Jakarta Data introduces a user-friendly and intuitive approach to handling pagination, making it easier for developers to manage large datasets efficiently. Eclipse JNoSQL extends Jakarta Data’s capabilities to support pagination within NoSQL databases, enhancing the overall data management experience.

[Click on the image to view full-size]

  • Jakarta NoSQL: On the other hand, Jakarta NoSQL focuses on working with NoSQL databases. It offers a fluent API that simplifies the interaction with various NoSQL databases. This API provides a consistent and intuitive way to perform operations and queries within the NoSQL data model. By leveraging Jakarta NoSQL, developers can harness the power of NoSQL databases while enjoying the benefits of a standardized and cohesive API, reducing the learning curve associated with working with different NoSQL databases.

Eclipse JNoSQL provides comprehensive support for integrating Java applications with persistence layers. Jakarta Data enables seamless integration across different persistence layers, and Jakarta NoSQL specifically caters to NoSQL databases. These specifications enhance developer productivity, reduce complexity, and promote interoperability within the Jakarta ecosystem, empowering developers to work efficiently with traditional and NoSQL data stores.

What’s New in Eclipse JNoSQL 1.0.0

Eclipse JNoSQL 1.0.0 has some exciting features. These upgrades improve the framework’s abilities and simplify connecting Java with NoSQL databases.

  • More straightforward database configuration: One of the notable enhancements is the introduction of simplified database configuration. Developers can now easily configure and connect to NoSQL databases without the need for complex and time-consuming setup procedures. This feature significantly reduces the initial setup overhead and allows developers to focus more on the core aspects of their application development.
  • Improved Java Record support: The latest update  includes enhanced support for Java Records – a new feature introduced in Java 14 that allows for the concise and convenient creation of immutable data objects. This update enables developers to easily map Java Records to NoSQL data structures, making data handling more efficient and effortless. This improvement also leads to better code readability, maintainability, and overall productivity in development.
  • Several bug fixes: Eclipse JNoSQL 1.0.0 introduces new features and fixes several bugs reported by the developer community.
  • Enhanced repository interfaces: The latest version comes with improved repository interfaces that effectively connect Java applications and NoSQL databases. These interfaces support a higher level of abstraction, making it easier for developers to interact with databases, retrieve and store data, and perform query operations. The updated repository interfaces in Eclipse JNoSQL also offer enhanced functionality, providing developers with greater flexibility and ease of performing database operations.

Eclipse JNoSQL 1.0.0 has introduced new features that improve the integration between Java and NoSQL databases. With these enhancements, developers can more efficiently utilize the full potential of NoSQL databases in their Java applications. These improvements also allow developers to focus on building innovative solutions rather than dealing with database integration complexities.

Show Me the Code

We will now dive into a live code session where we create a simple Pet application that integrates with a MongoDB database. While we acknowledge the popularity of MongoDB, it’s important to note that the concepts discussed here can be applied to other document databases, such as ArangoDB.

Before we proceed, it’s essential to ensure that the minimum requirements for Eclipse JNoSQL 1.0.0 are met. This includes Java 17, the Jakarta Contexts and Dependency Injection (CDI) specification, the Jakarta JSON Binding (JSON-B) specification and the Jakarta JSON Processing (JSON-P) specification that are compatible with Jakarta EE 10. Additionally, the Eclipse MicroProfile Config specification, version 3.0 or higher, is also required. Any Jakarta EE vendor compatible with version 10.0 and Eclipse MicroProfile Config 3.0 or any MicroProfile vendor compatible with version 6.0 or higher can run Eclipse JNoSQL. This broad compatibility allows flexibility in choosing a compatible Jakarta EE or MicroProfile vendor.

[Click on the image to view full-size]

It’s important to note that while we focus on the live code session, this article will not cover the installation and usage of MongoDB in production or recommend specific solutions like DBaaS with MongoDB Atlas. For demonstration purposes, feel free to install and use MongoDB in any preferred way. The article will use a simple Docker command to set up a database instance.

docker run -d --name mongodb-instance -p 27017:27017 mongo

Now that we have met the prerequisites, we are ready to proceed with the live code session, building and executing the Pet application that leverages Eclipse JNoSQL to interact with the MongoDB database.

In the next step, we will include Eclipse JNoSQL into the project, making it easier to handle dependencies and simplify the configuration process. The updated version of Eclipse JNoSQL streamlines the inclusion of dependencies, eliminating the need to individually add multiple dependencies.

To get started, we can explore the available databases repositories to determine which database we want to use and the required configurations. In our case, we will be using the MongoDB database. You can find MongoDB’s necessary credentials and dependencies at this GitHub repository.

If you are using a Maven project, you can include the MongoDB dependency by adding the following dependency to your project’s pom.xml file:


  org.eclipse.jnosql.databases
  jnosql-mongodb
  1.0.0

By adding this dependency, Eclipse JNoSQL will handle all the configurations and dependencies required to work with MongoDB in your project. This streamlined process simplifies the setup and integration, allowing you to focus on developing your Pet application without getting caught up in complex dependency management.

With Eclipse JNoSQL integrated into the project and the MongoDB dependency added, we are ready to explore leveraging its features and effectively interacting with the MongoDB database.

When integrating Eclipse JNoSQL with the MongoDB database, we can use the power of the Eclipse MicroProfile Config specification to handle the necessary credentials and configuration information. Eclipse MicroProfile Config allows us to conveniently overwrite these configurations through system environment variables, providing flexibility and adhering to the principles and practice of the Twelve-Factor App.

For example, we can define the MongoDB connection’s database name and host URL in an Eclipse MicroProfile Config configuration file (usually microprofile-config.properties). Here are the sample configurations:

jnosql.document.database=pets
jnosql.mongodb.host=localhost:27017

These configurations specify that the database name is pets and the MongoDB host URL is localhost:27017. However, instead of hardcoding these values, Eclipse MicroProfile Config can overwrite them based on system environment variables. This approach allows the development team to dynamically configure the database credentials and connection details without modifying the application code. The configurations will automatically be overwritten at runtime by setting environment variables, such as JNOSQL_DOCUMENT_DATABASE and JNOSQL_MONGODB_HOST. This flexibility ensures easy configuration management across different environments without requiring manual changes.

The combination of Eclipse MicroProfile Config and Eclipse JNoSQL enables the development of Twelve-Factor Apps, which adhere to best practices for modern cloud-native application development. It provides a seamless way to handle configuration management, making it easier to adapt the application to different environments and promoting a consistent deployment process. We can achieve a highly configurable and portable solution that aligns with the principles of modern application development.

To proceed with the implementation, we will create a model for our sample application, focusing on the pet world. In this case, we will restrict the pets to cats and dogs, and we will define their name and breed, which cannot be changed once created.

We will utilize Java records to achieve immutability, which provides a concise and convenient way to define immutable data objects. We will create two records, Dog and Cat, implementing the Pet interface.

Here is an example of the code structure:
 

public sealed interface Pet permits Cat, Dog {

    String name();
    String breed();
}

@Entity
public record Dog(@Id String id, @Column String name, @Column String breed) implements Pet {

    public static Dog create(Faker faker) {
        var dog = faker.dog();
        return new Dog(UUID.randomUUID().toString(), dog.name(), dog.breed());
    }
}

@Entity
public record Cat(@Id String id, @Column String name, @Column String breed) implements Pet {

    public static Cat create(Faker faker) {
        var cat = faker.cat();
        return new Cat(UUID.randomUUID().toString(), cat.name(), cat.breed());
    }
}

The above code defines the Pet interface as a sealed interface, allowing only the Cat and Dog records to implement it. Both records contain the field’s id, name, and breed. The @Id and @Column annotations mark the fields as identifiers and persistable attributes, respectively. The records also implement the Pet interface.

Additionally, static factory methods are included in each record to generate a new instance of a Dog or a Cat using a Faker object from the Java Faker project, a library for generating fake data.

With this modeling structure, we achieve immutability, define the necessary annotations to mark the classes as entities, and provide the essential attributes for persisting the pets in the database. This approach aligns with modern Java frameworks and facilitates the integration of the pet objects with the MongoDB database through Eclipse JNoSQL.

Now, let’s see the code in action! We can utilize the Template interface provided by Eclipse JNoSQL to seamlessly perform operations with the NoSQL database. Depending on the specific requirements, we can explore specialized interfaces such as DocumentTemplate for document databases or provider-specific templates like MongoDBTemplate for MongoDB.

An example code snippet demonstrating some essential operations using the Jakarta NoSQL API:

@Inject
Template template;

// ...

var faker = new Faker();
var cat = Cat.create(faker);
template.insert(cat);

Optional optional = template.find(Cat.class, cat.id());
System.out.println("The result: " + optional);

for (int index = 0; index < 100; index++) {
    template.insert(Cat.create(faker));
}
List result = template.select(Cat.class).where("breed").eq(cat.breed()).result();
System.out.println("The query by breed: " + result);

template.delete(Cat.class, cat.id());

In the code snippet above, we first inject the Template interface using CDI. Then, we create a new Cat object using the Java Faker library and insert it into the database using the insert() method.

We can retrieve the inserted Cat object from the database using the find() method, which returns an object of type Optional. In this case, we print the result to the console.

Next, we insert 100 more randomly generated cat objects into the database using a loop.

We query using the select() method, filtering the cat objects by the breed attribute. The result is stored in a list and printed to the console.

Finally, we delete the previously inserted cat object from the database using the delete() method.

Using the Jakarta NoSQL API and the Template interface, we can perform various operations with the NoSQL database without being aware of the specific implementation details. Eclipse JNoSQL handles the underlying database operations, allowing developers to focus on writing concise and efficient code for their applications.

This code demonstrates the power and simplicity of working with NoSQL databases using Eclipse JNoSQL in a Jakarta EE application.

Pagination is a common requirement when working with large datasets. In this case, we can leverage the Jakarta Data specification and the repository feature to seamlessly handle pagination. By creating interfaces that extend the appropriate repository interface, such as PageableRepository, the framework will automatically implement the necessary methods for us.

Here’s an example of how we can integrate pagination into our Cat and Dog repositories:

@Repository
public interface CatRepository extends PageableRepository, PetQueries {
}

@Repository
public interface DogRepository extends PageableRepository, PetQueries {
    default Dog register(Dog dog, Event event) {
        event.fire(dog);
        return this.save(dog);
    }
}

public interface PetQueries {
    List findByName(String name);

    List findByBreed(String breed);
}

In the code above, we define two repository interfaces, CatRepository and DogRepository, which extend the PageableRepository interface. It allows us to perform pagination queries on the Cat and Dog entities.

Additionally, we introduce a PetQueries interface that defines standard query methods for both Cat and Dog entities. This interface can be shared among multiple repositories, allowing code reuse and modularization.

In the DogRepository, we also showcase using default methods that have been available since Java 8. We define a custom method, register, which triggers an event and saves the dog object. It demonstrates the flexibility of adding custom business logic to the repository interface while benefiting from the framework’s underlying repository implementation.

By leveraging the repository feature and implementing the appropriate interfaces, Eclipse JNoSQL handles the implementation details for us. We can now seamlessly perform pagination queries and execute custom methods with ease.

This integration of pagination and repository interfaces demonstrates how Eclipse JNoSQL, in combination with Jakarta Data, simplifies the development process and promotes code reuse and modularization within the context of a Jakarta EE application.

Let’s put the pagination into action by injecting the DogRepository and using it to perform the pagination operations. The code snippet below demonstrates this in action:

@Inject
DogRepository repository;
@Inject
Event event;
var faker = new Faker();
var dog = Dog.create(faker);
repository.register(dog, event);
for (int index = 0; index < 100; index++) {
    repository.save(Dog.create(faker));
}
Pageable pageable = Pageable.ofSize(10).sortBy(Sort.asc("name"), Sort.asc("breed"));
Page dogs = repository.findAll(pageable);
while (dogs.hasContent()) {
    System.out.println("The page number: " + pageable.page());
    System.out.println("The dogs: " + dogs.stream().count());
    System.out.println("nn");
    pageable = pageable.next();
    dogs = repository.findAll(pageable);
}
repository.deleteAll();

In the code above, we first inject the DogRepository and Event using CDI. We then create a new dog using the Java Faker library and register it by calling the repository.register() method. The register() method also triggers an event using the Event object.

Next, we generate and save 100 more dogs into the database using a loop and the repository.save() method.

To perform pagination, we create a Pageable object with a page size of 10 and sort the dogs by name and breed in ascending order. We then call the repository.findAll() method passing the Pageable object to retrieve the first page of dogs.

We iterate over the pages using a while loop and print the page number and the number of dogs on each page. We update the Pageable object to the next page using the pageable.next() method and call repository.findAll() again to fetch the next page of dogs. Finally, we call repository.deleteAll() to delete all dogs from the database.

This code demonstrates the pagination feature, retrieving dogs in batches based on the defined page size and sorting criteria. It provides a convenient way to handle large datasets and display them to users in a more manageable manner.

In this code session, we witnessed the seamless integration between Eclipse JNoSQL and a MongoDB database in a Jakarta EE application. We explored the power of Eclipse JNoSQL, Jakarta Data, and Eclipse MicroProfile Config in simplifying the development process and enhancing the capabilities of our pet application.

The code showcased the modeling of pets using Java records and annotations, immutability, and entity mapping. We leveraged the Template interface to effortlessly perform operations with the MongoDB database. Pagination was implemented using Jakarta Data’s PageableRepository, providing an easy way to handle large datasets.

Using Eclipse MicroProfile Config enabled dynamic configuration management, allowing us to easily overwrite properties using system environment variables. This flexibility aligned with the Twelve-Factor App principles, making our application more adaptable and portable across environments.

The complete code for this session is available at jnosql-1-0-se. Additionally, you can explore more samples and demos with Java SE and Java EE at the following links: demos-se and demos-ee.

By utilizing Eclipse JNoSQL and its associated repositories, developers can harness the power of NoSQL databases while enjoying the simplicity and flexibility provided by the Jakarta EE and MicroProfile ecosystems. 

Eclipse JNoSQL empowers developers to focus on business logic and application development, abstracting away the complexities of NoSQL integration and allowing for a seamless exploration of polyglot persistence.

Quarkus Integration

One of the standout features in this release is the integration of Eclipse JNoSQL with Quarkus, a popular and highly efficient framework in the market. This integration is available as a separate module, providing seamless compatibility between Eclipse JNoSQL and Quarkus.

Integrating with Quarkus expands the possibilities for using Eclipse JNoSQL in your applications. You can now leverage the power of Eclipse JNoSQL with Quarkus’ lightweight, cloud-native runtime environment. The integration module currently supports ArangoDB, DynamoDB, Redis, Elasticsearch, MongoDB, and Cassandra databases, thanks to the significant contributions from Maximillian Arruda and Alessandro Moscatelli.

To stay updated on the communication and new updates between Quarkus and Eclipse JNoSQL, you can follow the repository at quarkiverse/quarkus-jnosql.

To start a project from scratch, you can explore the Quarkus extension for Eclipse JNoSQL at Quarkus.io – Eclipse JNoSQL.

To conclude this session, let’s look at three classes: Fish as an entity, FishService as a service, and FishResource as a resource. We can create a REST API for managing fish data with these classes. This sample introduces a fun twist by focusing on fish as pet animals, adding a post-tech pet-friendly touch.

@Entity
public class Fish {
    @Id
    public String id;
    @Column
    public String name;
    @Column
    public String color;
    // getters and setters
}
@ApplicationScoped
public class FishService {
    @Inject
    private DocumentTemplate template;
    private Faker faker = new Faker();
    public List findAll() {
        return template.select(Fish.class).result();
    }
    public Fish insert(Fish fish) {
        fish.id = UUID.randomUUID().toString();
        return this.template.insert(fish);
    }
    public void delete(String id) {
        this.template.delete(Fish.class, id);
    }
    public Optional findById(String id) {
        return this.template.find(Fish.class, id);
    }
    // other methods
}
@Path("/fishes")
@ApplicationScoped
public class FishResource {
    @Inject
    private FishService service;
    @GET
    @Path("{id}")
    public Fish findId(@PathParam("id") String id) {
        return service.findById(id)
                .orElseThrow(() -> new WebApplicationException(Response.Status.NOT_FOUND));
    }
    @GET
    @Path("random")
    public Fish random() {
       return service.random();
    }
    @GET
    public List findAll() {
       return this.service.findAll();
    }
    @POST
    public Fish insert(Fish fish) {
        fish.id = null;
        return this.service.insert(fish);
    }
    @PUT
    @Path("{id}")
    public Fish update(@PathParam("id") String id, Fish fish) {
       return this.service.update(id, fish)
               .orElseThrow(() -> new WebApplicationException(Response.Status.NOT_FOUND));
    }
    @DELETE
    @Path("{id}")
    public void delete(@PathParam("id") String id) {
        this.service.delete(id);
    }
}

The provided classes demonstrate the implementation of a REST API for managing fish data. The Fish class represents the entity, with name, color, and id as its properties. The FishService class provides methods for interacting with the fish data using the DocumentTemplate. Finally, the FishResource class serves as the REST resource, handling the HTTP requests and delegating the operations to the FishService.

You can find the detailed code for this example at quarkus-mongodb. With this code, you can explore the integration between Eclipse JNoSQL and Quarkus, building a pet-friendly REST API for managing fish data.

Conclusion

The release of Eclipse JNoSQL 1.0.0 marks a significant milestone in NoSQL database integration with Java applications. With its rich features and seamless compatibility with Jakarta EE, Eclipse MicroProfile, and now Quarkus, Eclipse JNoSQL empowers developers to leverage the full potential of NoSQL databases in modern enterprise applications.

The integration with Quarkus opens up new possibilities for developers, allowing them to harness the power of Eclipse JNoSQL in Quarkus’ lightweight, cloud-native runtime environment. With support for popular databases like ArangoDB, DynamoDB, Redis, Elasticsearch, MongoDB, and Cassandra, developers can choose the suitable database for their specific needs and easily switch between them.

Throughout this article, we explored the core features of Eclipse JNoSQL, including the unified API for various NoSQL databases, the use of Jakarta Data and repository interfaces for seamless integration, and the benefits of using Java records and annotations for entity mapping and immutability.

We witnessed how Eclipse JNoSQL simplifies the development process, abstracting away the complexities of NoSQL database integration and allowing developers to focus on writing clean, efficient code. The seamless integration with Eclipse MicroProfile Config further enhances flexibility, enabling dynamic configuration management.

Moreover, the demonstration of building a pet-friendly REST API using Eclipse JNoSQL showcased its simplicity and effectiveness in real-world scenarios. We explored the usage of repositories, pagination, and standard query methods, highlighting the power of Eclipse JNoSQL in handling large datasets and delivering efficient database operations.

As Eclipse JNoSQL continues to evolve, it offers developers an extensive range of options and flexibility in working with NoSQL databases. The vibrant community and ongoing contributions ensure that Eclipse JNoSQL remains at the forefront of NoSQL database integration in the Java ecosystem.

Eclipse JNoSQL 1.0.0 empowers developers to seamlessly integrate NoSQL databases into their Java applications, providing a powerful and efficient solution for modern enterprise needs.

Monitoring Critical User Journeys in Azure

Key Takeaways

  • ​​A critical user journey (CUJ) is an approach that maps out the key interactions between users and a product. CUJs are a great way to understand the effectiveness of application flows and identify bottlenecks.
  • Tools like Prometheus and Grafana provide a standardized way to collect and visualize these metrics and monitor CUJs.
  • In the Flowe technology stack, fulfilling a CUJ often involves a user’s request being handled by a mix of PaaS and Serverless technology.
  • In the current economic climate, pricing is a critical factor for any monitoring solution. The decision of build vs buy must be analyzed closely, even when running initial experiments.
  • Infrastructure as Code (IaC) frameworks like Azure Bicep help provision resources and organize CUJ metric collection as resources are deployed.

 

The Need for Application Monitoring

I work as an SRE  for Flowe , an Italian challenger digital bank where the software, rather than physical bank branches, is the main product. Everything is in the mobile app, and professionally speaking, ensuring continued operation of this service is my main concern. Unlike a traditional bank, customers rely on this mobile app as a key point of interaction with our services.

Flowe established a monitoring team to ensure proper integration between the bank platform and its numerous third-party services (core banking, card issuer, etc.). This team is available 24/7, and when something goes wrong on the third-party systems (i.e., callbacks are not working), they open a ticket on third-party systems.

Although they do an excellent job, the monitoring team doesn’t have deep knowledge about the system architecture, business logic, or even all the components of the bank platform. Their scope is limited to third parties.

This means that if a third party is not responding, they are quick to open tickets and monitor them until the incident is closed. However, the monitoring team lacks the development skills and knowledge to catch bugs, improve availability/deploy systems, measure performances, monitor dead letter queues (DLQs), etc. For this reason, at least initially, when Flowe launched, senior developers were in charge of these tasks.

However, after the first year of life, we realized developers were too busy building new features, etc., and they didn’t have time for day-to-day platform observation. So we ended up creating our SRE team with the primary goal of making sure the banking platform ran smoothly.

SRE Team Duties

What Flowe needed from an SRE team changed over time. As explained in the previous paragraph, the first necessity was covering what developers and the monitoring team couldn’t do: monitor exceptions and API response status code, find bugs, watch the Azure Service Bus  DLQs, measure performances, adopt infrastructure as code (IaC), improve deployment systems and ensure high availability.

The transition of responsibilities toward the SRE team had been slow but efficient, and over time the SRE team has grown, expanding the skill set and covering more and more aspects of Flowe’s daily routine. We started to assist the “caring team” (customer service) and put what is called Critical User Journeys (CUJ) in place.

CUJs are a great way to understand the effectiveness of application flows and identify bottlenecks. One example of a CUJ in our case is the “card top up process”, an asynchronous flow that involves different actors and services owned by many organizations. This CUJ gives us the context of the transaction and enables us to understand where users encounter issues and what the bottlenecks are. Solving issues rapidly is extremely important. Most users that get stuck in some process don’t chat with the caring team but simply leave a low app rating.

SRE Team Tools

Azure Application Insights  is an amazing APM tool, and we use it intensively for diagnostic purposes within our native iOS/Android SDK. However, Although we had decided to use Application Insights, the integration into the core Azure Monitor suite lacked some features that were critical to our usage.

For example, you can only send alerts via emails and SMS, and there is no native integration with other services such as PagerDuty, Slack, or anything else. Moreover, creating a custom dashboard using an Azure Workbook is only flexible and scalable in some environments because of their limited flexibility.

For all of the mentioned reasons, we, as the SRE team, decided to put in place two well-known open-source products to help us with monitoring and alerting tasks: Prometheus  and Grafana.

Prometheus is an open-source project hosted by the Cloud Native Computing Foundation (CNCF). Prometheus uses the pull model to collect metrics on configured targets and save data in its time series database. Prometheus has its data model – a plain text format – and as long as you can convert your data to this format, you can use Prometheus to collect and store data.

Grafana is another CNCF project that allows you to create dashboards to visualize data collected from hundreds of different places (data sources). Usually, Grafana is used with Prometheus since it can understand and display its data format. Still, many other data sources, such as Azure Monitor, Azure DevOps, DataDog, and GitHub, can be used. Grafana handles alerts, integrating with many services such as Teams or Slack.

Monitoring Solution

As we adopted Prometheus and Grafana, we needed to ensure that we did this in a cost-effective manner – the two key metrics were the size of our team and the amount of data processed/stored. So we did some proof of concept using an Azure Virtual Machine (VM)  with a Docker Compose file to start both Prometheus and Grafana containers and practice with them. The solution was cheap, but managing an entire VM to run two containers wastes time.

For this reason, we looked at the managed version offered by Azure:

  • Grafana Managed Instance costs 6€/month/active user – a bit more expensive than the 30€ for the monthly VM as the SRE team consists of six people.
  • Prometheus Managed pricing model  is not straightforward, especially if you are starting from scratch and don’t have any available metrics to rely on. Moreover, you have to pay for notifications (push, emails, webhooks, etc.).

After some experiments and research on all possible solutions, we saw that a combination of SaaS and serverless Azure solutions seemed the best option for our use case. Let’s see which ones, how, and why they’ve been integrated with each other.

Azure Managed Grafana

Azure Managed Grafana is a fully managed service for monitoring solutions. So, actually, we found what we were looking for: automatic software upgrades, SLA guarantees, availability zones, Single Sign-On with AAD  and integration with Azure Monitor (via Managed Identity ) ready out of the box.

Provisioning Grafana using Bicep

Bicep provides a simple way of provisioning our cloud resources through Infrastructure as Code principles. This allows repeatable deployments as well as a way to record the resource’s place along the CUJ. The Bicep definition of a Grafana Managed instance is simple.

resource grafana 'Microsoft.Dashboard/grafana@2022-08-01' = {
  name: name
  location: resourceGroup().location
  sku: {
    name: ‘Standard’
  }
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    apiKey: 'Disabled'
    autoGeneratedDomainNameLabelScope: 'TenantReuse'
    deterministicOutboundIP: 'Enabled'
    publicNetworkAccess: 'Enabled'
    zoneRedundancy: 'Enabled'
  }
}

In this configuration, it is worth highlighting in detail: the `deterministicOutbountIP` is set to `Enabled`. This allows us to have two static outbound IPs that we will use later to isolate the Prometheus instance from Grafana.

Finally, we needed to grant the Grafana Admin Role to our AAD group and the Monitoring Reader Role to the Grafana Managed Identity to get access to Azure Monitor logs.

@description('This is the built-in Grafana Admin role. See https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles')
resource grafanaAdminRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
  name: '22926164-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
}

@description('This is the built-in Monitoring Reader role. See https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#monitoring-reader')
resource monitoringReaderRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
  scope: subscription()
  name: '43d0d8ad-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
}

@description('This is the SRE AD group.')
resource sreGroup 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
  name: 'aed71f3f-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
}

resource adminRoleSreGroupAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(subscription().id, sreGroup.id, grafanaAdminRole.id)
  properties: {
    roleDefinitionId: grafanaAdminRole.id
    principalId: sreGroup.name
    principalType: 'Group'
  }

  dependsOn: [
    grafana
  ]
}

resource monitoringRoleSubscription 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(subscription().id, grafana.name, monitoringReaderRole.id)
  properties: {
    roleDefinitionId: monitoringReaderRole.id
    principalId: grafana.outputs.principalId
    principalType: 'ServicePrincipal'
  }

  dependsOn: [
    grafana
  ]
}

Azure Container Apps (ACA) for Prometheus

Azure Container Apps is a serverless solution to run containers on Azure. Under the hood, it runs containers on top of Kubernetes; it is completely managed by Azure and the Kubernetes API (and any associated complexity) is never exposed to the users. Plus, it can scale from 0 to 30 replicas (0 is for free!), and you can attach volumes via a Files Share mounted on a Storage Account (a necessary option to run Prometheus as it is a time series database). We chose this service for its simplicity and flexibility, within the cost of around 20€/month when turned on 24/7.

Provisioning Prometheus on ACA using Bicep

We start to define a Storage Account that will be used to mount a volume on the container, initially allowing connections for outside.

resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
  name: storageAccountName
  location: resourceGroup().location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_ZRS'
  }
  properties: {
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
    networkAcls: {
      defaultAction: 'Allow'
    }
    largeFileSharesState: 'Enabled'
  }
}

And then the related File Share using the SMB protocol.

resource fileServices 'Microsoft.Storage/storageAccounts/fileServices@2022-09-01' = {
  name: 'default'
  parent: storageAccount
}

resource fileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2022-09-01' = {
  name: name
  parent: fileServices
  properties: {
    accessTier: 'TransactionOptimized'
    shareQuota: 2
    enabledProtocols: 'SMB'
  }
}

Selecting the right `accessTier` here is important: we chose the `Hot` option, but it was an expensive choice with no performance gain. `TransactionOptimized` is much cheaper and more suited to Prometheus’s work. 

This File Share resource will be mounted on the container, so it shall arrange the local environment for Prometheus by provisioning two folders: `data` and `config`. In my case, the latter must contain the Prometheus configuration file named `prometheus.yml`. 

The former is used to store the time series database. In our Bicep file, we launch a shell script through a Bicep Deployment Script  to ensure these prerequisites exist at each pipeline run. And finally, the container app with the accessory resources – environment and log analytics workspace.

resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
  name: containerAppLogAnalyticsName
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
  }
}

var vnetConfiguration = {
  internal: false
  infrastructureSubnetId: subnetId
}

resource containerAppEnv 'Microsoft.App/managedEnvironments@2022-10-01' = {
  name: containerAppEnvName
  location: resourceGroup().location
  sku: {
    name: 'Consumption'
  }
  properties: {
    appLogsConfiguration: {
      destination: 'log-analytics'
      logAnalyticsConfiguration: {
        customerId: logAnalytics.properties.customerId
        sharedKey: logAnalytics.listKeys().primarySharedKey
      }
    }
    vnetConfiguration: vnetConfiguration
  }
}

resource permanentStorageMount 'Microsoft.App/managedEnvironments/storages@2022-10-01' = {
  name: storageAccountName
  parent: containerAppEnv
  properties: {
    azureFile: {
      accountName: storageAccountName
      accountKey: storageAccountKey
      shareName: fileShareName
      accessMode: 'ReadWrite'
    }
  }
}

resource containerApp 'Microsoft.App/containerApps@2022-10-01' = {
  name: containerAppName
  location: resourceGroup().location
  properties: {
    managedEnvironmentId: containerAppEnv.id
    configuration: {
      ingress: {
        external: true
        targetPort: 9090
        allowInsecure: false
        ipSecurityRestrictions: [for (ip, index) in ipAllowRules: {
          action: 'Allow'
          description: 'Allow access'
          name: 'GrantRule${index}'
          ipAddressRange: '${ip}/32'
        }]
        traffic: [
          {
            latestRevision: true
            weight: 100
          }
        ]
      }
    }
    template: {
      revisionSuffix: toLower(utcNow())
      containers: [
      {
        name: 'prometheus'
        probes: [
          {
            type: 'liveness'
            httpGet: {
              path: '/-/healthy'
              port: 9090
              scheme: 'HTTP'
            }
            periodSeconds: 120
            timeoutSeconds: 5
            initialDelaySeconds: 10
            successThreshold: 1
            failureThreshold: 3
          }
          {
            type: 'readiness'
            httpGet: {
              path: '/-/ready'
              port: 9090
              scheme: 'HTTP'
            }
            periodSeconds: 120
            timeoutSeconds: 5
            initialDelaySeconds: 10
            successThreshold: 1
            failureThreshold: 3
          }
        ]
        image: 'prom/prometheus:latest'
        resources: {
          cpu: json('0.75')
          memory: '1.5Gi'
        }
        command: [
          '/bin/prometheus'
          '--config.file=config/prometheus.yml'
          '--storage.tsdb.path=data'
          '--storage.tsdb.retention.time=60d'
          '--storage.tsdb.no-lockfile'
        ]
        volumeMounts: [
          {
            mountPath: '/prometheus'
            volumeName: 'azurefilemount'
          }
        ]
      }
    ]

      volumes: [
      {
        name: 'azurefilemount'
        storageType: 'AzureFile'
        storageName: storageAccountName
      }
    ]
      scale: {
        minReplicas: 1
        maxReplicas: 1
      }
    }
  }
  dependsOn: [
    permanentStorageMount
  ]
}

The above script is a bit long but hopefully still easy to understand. (If not, check out the documentation.) However, some details are worth highlighting and explaining.

Since we don’t need to browse the Prometheus dashboard, the ACA firewall should be enabled to block traffic from anything except Grafana, which is configured to use two outbound static IPs (passed as parameter `ipAllowRules`). 

To achieve this result, ingress must be enabled (`ingress`:`external` equals `true`). The same must be done for the underlying Storage Account. However, at the time of writing, isolation between ACA and Storage Accounts is not supported yet. This option is available just for a few services, such as VNets. 

For this reason, we had to isolate ACA in a VNet (unfortunately /23 size is a requirement ). Due to a Bicep bug, this option will not work if defined at the first Storage Account definition already written above. Still, it must be repeated at the end of the Bicep script.

resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
  name: storageAccountName
  location: resourceGroup().location
  kind: 'StorageV2'
  sku: {
    name: storageAccountSku
  }
  properties: {
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
    networkAcls: {
      defaultAction: 'Deny'
      bypass: 'AzureServices'
      virtualNetworkRules: [for i in range(0, length(storageAccountAllowedSubnets)): {
        id: virtualNetworkSubnets[i].id
      }]
    }
    largeFileSharesState: 'Enabled'
  }
}

Wrapping out the monitoring solution

What is described above so far could be represented diagrammatically in this way:

[Click on the image to view full-size]

  • The SRE AD group and related Managed Identity used for automation can access Grafana through AAD
  • Grafana can access Azure Monitor using Managed Identity: metrics, logs, and resource graphs can be queried using Kusto
  • Grafana IPs are allowed to connect with Prometheus ACA, hosted in a custom VNet
  • File Share mounted on a Storage Account is used as volume to run Prometheus container
  • Potentially, Prometheus can be scaled up and down – until now, we haven’t needed to do this

At this point a question arises: how can Prometheus access the Bank Platform – hosted in a closed VNet – to collect aggregate data?

This can be addressed with a different serverless solution: Azure Functions.

Monitoring Function App

An Azure Function app hosted in the Bank Platform VNet would be able to collect data from all the productive components around the platform: it has access to Azure SQL Database, Cosmos DB, Azure Batch, Service Bus, and even the Kubernetes cluster. 

It would be possible to query different data using a combination of tools such as Azure SDKs, and so why did we choose to use a Function app? Because it can expose REST APIs to the scheduled Prometheus jobs, and when these jobs are paused or stopped, Function apps are for free, being a serverless solution. Moreover, we could configure the Functions app to accept connections from a specific VNet only, the Prometheus VNet in this case.

Then, the complete diagram appears this way:

[Click on the image to view full-size]

In our case, the Monitoring Function app runs on .NET 7  using the new isolated worker process.

In `Program.cs`, create and run the host.

var host = Host.CreateDefaultBuilder()
    .ConfigureAppConfiguration((ctx, builder) =>
    {
        if (ctx.HostingEnvironment.IsDevelopment())
        {
            builder.AddUserSecrets(Assembly.GetExecutingAssembly(), optional: false);
            return;
        }

        // On Net7, it is fast enough to be used
        var configuration = builder.Build()!;
        // This logger is useful when diagnostic startup issues on Azure Portal
        var logger = LoggerFactory.Create(config =>
        {
            config.AddConsole();
            config.AddConfiguration(configuration.GetSection("Logging"));
        })
        .CreateLogger("Program");

        logger.LogInformation("Environment: {env}", ctx.HostingEnvironment.EnvironmentName);

        builder.AddAzureAppConfiguration(options =>
        {
            
            options.ConfigureRefresh(opt =>
            {
                // Auto app settings refresh
            });

            options.ConfigureKeyVault(opt =>
            {
                // KeyVault integration
            });
        }, false);
    })
    .ConfigureServices((ctx, services) =>
    {
        // Register services in IoC container
    })
    .ConfigureFunctionsWorkerDefaults((ctx, builder) =>
    {
        builder.UseDefaultWorkerMiddleware();

        if (ctx.HostingEnvironment.IsDevelopment())
            return;

        string? connectionString = ctx.Configuration.GetConnectionString("ApplicationInsights:SRE");
        if (string.IsNullOrWhiteSpace(connectionString))
            return;

        builder
            .AddApplicationInsights(opt =>
            {
                opt.ConnectionString = connectionString;
            })
            .AddApplicationInsightsLogger();

        builder.UseAzureAppConfiguration();
    })
    .Build();

await host.RunAsync();

Each function namespace represents a different monitoring context, so we have, for example, a namespace dedicated to the Azure Service Bus and another for Azure Batch, and so on. All namespaces provide an extension method to register into `IServiceCollection` all the requirements it needs. These extension methods are called from the `ConfigureServices`.

Monitoring Examples

Before concluding this, I want to provide some real usage examples.

Application Insights Availability Integration

Ping availability tests provided by Application Insights (AI) cost 0.0006€ per test. However, you can ping your services with a custom code and send the result to Application Insights using the official SDK for free.

Here is the code of the Availability section of the Monitoring Function app.

private async Task PingRegionAsync(
        string url,
        string testName)
    {
        const string LOCATION = "westeurope";

        string operationId = Guid.NewGuid().ToString("N");

        var availabilityTelemetry = new AvailabilityTelemetry
        {
            Id = operationId,
            Name = testName,
            RunLocation = LOCATION,
            Success = false,
            Timestamp = DateTime.UtcNow,
        };

        // not ideal, but we just need an estimation
        var stopwatch = Stopwatch.StartNew();

        try
        {
            await ExecuteTestAsync(url);

            stopwatch.Stop();

            availabilityTelemetry.Success = true;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();

            if (ex is HttpRequestException reqEx && reqEx.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                _logger.LogError(reqEx, "Probably a route is missing");
            }

            HandleError(availabilityTelemetry, ex);
        }
        finally
        {
            availabilityTelemetry.Duration = stopwatch.Elapsed;

            _telemetryClient.TrackAvailability(availabilityTelemetry);
            _telemetryClient.Flush();
        }
    }

    private async Task ExecuteTestAsync(string url)
    {
        using var cancelAfterDelay = new CancellationTokenSource(TimeSpan.FromSeconds(20));

        string response;

        try
        {
            response = await _httpClient.GetStringAsync(url, cancelAfterDelay.Token);
        }
        catch (OperationCanceledException)
        {
            throw new TimeoutException();
        }

        switch (response.ToLowerInvariant())
        {
            case "healthy":
                break;
            default:
                _logger.LogCritical("Something is wrong");
                throw new Exception("Unknown error");
        }
    }

    private void HandleError(AvailabilityTelemetry availabilityTelemetry, Exception ex)
    {
        availabilityTelemetry.Message = ex.Message;

        var exceptionTelemetry = new ExceptionTelemetry(ex);
        exceptionTelemetry.Context.Operation.Id = availabilityTelemetry.Id;
        exceptionTelemetry.Properties.Add("TestName", availabilityTelemetry.Name);
        exceptionTelemetry.Properties.Add("TestLocation", availabilityTelemetry.RunLocation);
        _telemetryClient.TrackException(exceptionTelemetry);
    }

Initially, an `AvailabilityTelemetry` object is created and set up. Then, the ping operation is performed; depending on the result, different information is stored in AI using the SDK’s objects.

Note that the `stopwatch` object is not accurate, but it is enough for our use case.

Card Top up Critical User Journey

This is an example of a Critical User Journey (CUJ) where a user wants to top up their bank account through an external service. Behind the scenes, a third-party service notifies Flowe about the top up via REST APIs. Having access to Azure Monitor and from Grafana, it’s simply displaying the count of callbacks received through a Kusto query against the Application Insights resource.

requests
| where (url == "")
| where timestamp >= $__timeFrom and timestamp < $__timeTo
| summarize Total = count()

[Click on the image to view full-size]

It is also possible to display the same data as a time series chart but group callbacks by their status code.

requests
| where (url == "")
| where timestamp >= $__timeFrom and timestamp < $__timeTo
| summarize response = dcount(id) by resultCode, bin(timestamp, 1m)
| order by timestamp asc

[Click on the image to view full-size]

After the callback is received, a Flowe internal asynchronous flow is triggered to let microservices communicate with each other through integration events .

To complete this CUJ, the same Grafana dashboard shows the number of items that ended up in DLQ due to failures. Azure Monitor does not expose this kind of data directly, so custom code had to be written. The Monitor Functions app exposes an endpoint to return aggregate data of the items stuck in the Azure Service Bus DLQs.

[Function(nameof(DLQFunction))]
    public async Task RunAsync(
        [HttpTrigger(
            AuthorizationLevel.Function,
            methods: "GET",
            Route = "exporter"
        )] HttpRequestData req)
    {
        return await ProcessSubscriptionsAsync(req);
    }

    private async Task ProcessSubscriptionsAsync(HttpRequestData req)
    {
        var registry = Metrics.NewCustomRegistry();
        _gauge = PrometheusFactory.ProduceGauge(
            registry,
            PrometheusExporterConstants.SERVICEBUS_GAUGE_NAME,
            "Number of DLQs grouped by subscription and subject",
            labelNames: new[]
                {
                    PrometheusExporterConstants.SERVICEBUS_TYPE_LABEL,
                    PrometheusExporterConstants.SERVICEBUS_TOPIC_LABEL,
                    PrometheusExporterConstants.SERVICEBUS_SUBSCRIPTION_LABEL,
                    PrometheusExporterConstants.SERVICEBUS_SUBJECT_LABEL,
                });

        foreach (Topic topic in _sbOptions.serviceBus.Topics!)
        {
            foreach (var subscription in topic.Subscriptions!)
            {
                await ProcessSubscriptionDlqs(_sbOptions.serviceBus.Name!, topic.Name!, subscription, _gauge);
            }
        }

        return await GenerateResponseAsync(req, registry);
    }

    private async Task ProcessSubscriptionDlqs(string serviceBus, string topic, string subscription, Gauge gauge)
    {
        var stats = await _serviceBusService.GetDeadLetterMessagesRecordsAsync(serviceBus, topic, subscription);

        var groupedStats = stats
            .GroupBy(x => x.Subject, (key, group) => new { Subject = key, Count = group.Count() })
            .ToList();

        foreach (var stat in groupedStats)
        {
            gauge!
                .WithLabels("dlqs", topic, subscription, stat.Subject)
                .Set(stat.Count);
        }
    }

    private static async Task GenerateResponseAsync(HttpRequestData req, CollectorRegistry registry)
    {
        var result = await PrometheusHelper.GenerateResponseFromRegistryAsync(registry);

        var response = req.CreateResponse(HttpStatusCode.OK);
        response.Headers.Add("Content-Type", $"{MediaTypeNames.Text.Plain}; charset=utf-8");
        response.WriteString(result);

        return response;
    }

In this case, the Function App doesn’t query the Azure Service Bus instance directly; it is instead wrapped by another custom service through the `_serviceBusService`.

P.S. We are working to publish this service on GitHub!

Once data is returned to Prometheus, Grafana can show them using PromQL.

service_bus{subject="CriticalEventName"}

[Click on the image to view full-size]

OnBoarding Critical User Journey

The “OnBoarding” CUJ starts when customers open the app for the first time and finishes when the customer successfully opens a new bank account. It is a complicated journey because the user’s digital identity must be confirmed, and personal data is processed by Know Your Customer (KYC) and anti-money laundering services. To complete the process, a lot of third parties are involved.

Here, I want to share a piece of this CUJ dashboard where a sequence of APIs is monitored, and a funnel is built on top of them, among other data.

[Click on the image to view full-size]

 

The queries used to build this dashboard are similar to those described above.

Conclusions

“Critical User Journeys are an effective way to decide what metrics monitor and guide their collection. Bringing these metrics together in tools like Prometheus and Grafana simplifies the way in which SREs and Architects can exchange responsibilities to oversee operations. Custom code may be needed to collect certain metrics about different CUJs, but all teams benefit from the resulting simplicity in monitoring the overall workflow.

Usually, Prometheus and Grafana are used to monitor Kubernetes and application metrics such as API average response time and CPU under use.

Instead, this post shows how to calculate and display CUJs over aggregated data, necessarily collected using custom code.

Architectural choices point to a cost-effective solution in Azure, but keep in mind the deployment simplicity and requirements that an organization may have (such as SSO and security concerns). Plus, the need for maintenance is almost eliminated.”

Note: All the numbers and names of the screenshots shown in this article were taken from test environments (and sometimes mixed as well). There is no way to get real data and real flows from the charts above.

The Role of Digital Twins in Unlocking the Cloud’s Potential

Key Takeaways

  • Digital twins are virtual models that mirror physical entities, enhancing their capabilities by providing a behavior modeling, prediction, and optimization platform. They have been successfully implemented in various fields, such as IoT, smart factories, healthcare, and urban development.
  • The concept of digital twins is not limited to physical objects. It’s already prevalent in software domains, where digital representations exist for software entities at various levels of granularity, from broad systems like virtual machines to granular entity objects.
  • The Service Twin Pattern is an innovative concept that applies the principles of digital twins to microservices. It creates a 1:1 mapping between a microservice and its cloud-based twin, enhancing the service’s behavior with additional functionalities.
  • The Service Twin Pattern can revolutionize cloud services by making them more application-oriented and developer-friendly. By making the service a first-class entity in the cloud runtime, it aligns operational concerns to the service boundary, simplifying the use of cloud services.
  • The Service Twin Pattern has the potential to redefine how developers interact with cloud services. As it continues to evolve, it could pave the way for a new era of cloud services consumption, where services are the primary unit of functionality.

 

A digital twin (DT) is a virtual representation of a physical object, process, or system that mirrors its real-world counterpart in real-time, allowing for simulations, analysis, and optimization to improve performance and gain valuable insights.

What if the key to making the cloud application-oriented and developer-friendly was already here, hidden in the concept of digital twins? The digital twin concept is revolutionizing various fields, from the Internet of Things (IoT) and smart factories to healthcare, urban development, and the automotive industry. The power of enhancing a physical entity with a digital replica for functions such as behavior modeling, prediction, and optimization has opened new avenues of exploration and innovation.

This article explores using the DT concept as a new way to make cloud services more developer-friendly. This new model aligns the development, deployment, and now runtime aspects of a microservice into a single, cohesive unit, bridging the gap between developers and the cloud and paving the way for a new era of cloud services.

The transformative power of digital twins in the physical world

A DT, in essence, is a high-fidelity virtual model designed to mirror an aspect of a physical entity accurately. Let’s imagine a piece of complex machinery in a factory. This machine is equipped with numerous sensors, each collecting data related to critical areas of functionality from temperature to mechanical stress, speed, and more. This vast array of data is then transmitted to the machine’s digital counterpart. With this rich set of data, the DT becomes more than just a static replica. It evolves into a dynamic model that can simulate the machinery’s operation under various conditions, study performance issues, and even suggest potential improvements.

The ultimate goal of these simulations and studies is to generate valuable insights that can be applied to the original physical entity, enhancing its performance and longevity. The resulting architecture is a dual Cyber-Physical System with a constant flow of data that brings unique insights into the physical realm from the digital realm.

[Click on the image to view full-size]

A digital representation of a physical object

DTs have made significant headway in various industries. They are implemented in domains such as manufacturing, aerospace, healthcare, urban planning, and intelligent cars, to name a few. In manufacturing, DTs help model behavior, predict outcomes, and optimize processes. In healthcare, DTs of human organs can assist doctors in understanding a patient’s unique physiology, enabling personalized treatment plans. In urban planning, DTs of entire cities allow planners to simulate and analyze the impact of different development strategies. But does the realm of DTs extend only to physical objects? Interestingly, the concept of DTs is also prevalent in the realm of digital objects under different names. The following sections will delve into these questions.

Extending digital twins to the digital realm

The notion of a digital representation for a software entity is already prevalent in various software frameworks. Here, we can draw parallels between DTs and concepts varying in granularity from a broader system like a virtual machine to something as granular as a specific entity object. Let’s delve into examples that range across this spectrum.

Consider a virtual server equipped with a monitoring agent that establishes a 1:1 connection between the server and its remote representation. This agent not only streams metrics from the server to a central repository for analysis but also enables the execution of commands and facilitates server debugging when needed. The presence of this agent provides operations teams with valuable insights into the health and load of their servers. Armed with this information, they can effectively plan server maintenance activities, such as patching, upgrades, and scaling operations based on load. Essentially, this allows the operations team to model entire data centers composed of servers and continuously track their load, health, and retirement. This use case is primarily about the operation and maintenance of host machines.

Drilling down a level from the server to a specific process, we encounter another manifestation of the DT concept. Let’s visualize a large-scale distributed application running on Java. Monitoring services like Datadog, Dynatrace, and others operate by installing an agent into the Java virtual machine (JVM) and gathering detailed insights about its workings. These insights are then sent to a central server on the cloud, enabling each application’s performance to be modeled, understood, and optimized.

Yet other examples echo the essence of DT at the process level. A Service Mesh such as Istio employs the sidecar architecture, for example, to monitor and control a distributed system’s networking aspects. The Service Mesh architecture can model the interactions of all services in a way similar to how a smart city models the movement of cars on its streets.

Moving further down within a process, we encounter examples of DTs that help model and manage different aspects of applications. The Virtual Actor pattern, for instance, allows the use of an actor ID to represent the actor object while managing its activation, deactivation, placement, lifecycle, and more. The line gets blurry here, but other examples include an Object-Relational Mapping system such as Hibernate that allows objects to represent data in a state store or an entity workflow pattern that allows tracking and controlling the state transitions of an entity through a remote representation in the workflow engine.

[Click on the image to view full-size]

Digital twin concept applied to digital objects

All these examples show a common pattern – a 1:1 mapping with a piece of software, represented remotely by another software, focusing on one particular aspect of the software, such as lifecycle, health, state, networking, etc. A generalized version of this pattern applied to Microservices would be a new architectural concept – the Service Twin Pattern. Let’s look into that next.

Introducing a new architectural concept: the Service Twin Pattern

New requirements and software abstractions continually inspire the development of new innovative patterns and paradigms. So is the case with the Service Twin Pattern. But why coin a new term instead of leveraging the established, such as DT or Sidecar patterns? Patterns help to quickly communicate an idea and highlight certain aspects of a solution. The DT pattern emphasizes creating a digital counterpart for a non-digital entity, forming a Cyber-Physical System. Here, the presence of a non-digital object is key, hence the pattern’s name. And typically, the twin doesn’t play a direct role in the functioning of the physical counterpart but enriches its lifecycle, offering indirect optimization benefits. The Sidecar pattern, on the other hand, popularized by Kubernetes’ Pods concept and projects such as Dapr, extends an application’s capabilities at deployment time. These added capabilities are co-located with the main application, sharing the same deployment target and lifecycle.

While inspired by these two patterns, the Services Twin Pattern is at a service granularity. It doesn’t have a physical counterpart – nor is it limited to the sidecar architecture, hence the need for a new name that communicates its essence. Microservice has emerged as today’s universal unit of a digital object. The boundaries of a microservice delineate team ownership, lifecycle, scaling policies, deployment location, and many other attributes. As such, a microservice represents the ideal cohesive unit of digital functionality that can be remotely represented with a twin as a unit of operational control structure and consuming remote development capabilities. Herein lies the core of the Service Twin Pattern. This pattern creates a remote twin of a service, typically on cloud infrastructure, that uniquely represents the real service. By forming a 1:1 mapping between a service and its twin, this pattern can enhance the service’s behavior with additional functionalities without imposing any limits on the nature of these enhancements.

Fundamental to this unique mapping between the service and its remote twin representation is establishing a secure communication channel for data exchange. Similarly to the DT pattern with physical objects, the complexity of this data exchange can vary depending on the use case. In simpler instances like monitoring, the data flow could be unidirectional, emitting only metrics and logs from the service to its twin. The next level of complexity would be when the service twin controls and manages certain aspects of the service, as is the case with a Service Mesh controlling the networking policies. Some service twins can go beyond acting as a control structure and manage the configuration and secrets of a service. For example, HashiCorp Cloud Platform Vault has the concept of an application with associated secrets that get passed and any updates pushed to the real application at runtime.

In other cases, the data flow could be bidirectional; such is the case with Ably, who are providing real-time messaging capabilities. In Ably, there is no concept of message brokers but applications with queues, tokens, metrics, notifications, etc. In more advanced remote twins such as Diagrid Cloud Runtime APIs, the data flow can follow various patterns. The twin can provide many complementary capabilities such as offloading state management, workflows, connectors, etc., creating a dynamic dual-cyber system. Today, the application concept is present primarily in compute-centric cloud services such as AWS AppRunner, Google CloudRun, Azure Container Apps, Railway, Convex, and Vercel, to name a few. But having a top-level application concept would allow developers to bind their applications to their twin at runtime and combine them with additional capabilities as described in the cloud-bound architecture post.

[Click on the image to view full-size]

Service Twin Pattern on a cloud infrastructure

Consider this scenario: you have a service, and you create a corresponding service twin in a cloud environment. With the unique mapping and a secure communication channel established between the service and its twin (via an agent, sidecar, SDK, simple URL mappings, CLI-based configuration, etc.), the two can communicate effectively. Through its twin, the service can be configured to retrieve only its secrets defined in the cloud environment. The twin can assist in performing service discovery, configuration propagation, and connecting to 3rd party endpoints.

Furthermore, the service could offload state management to a key-value store in the cloud that is scoped to its twin only. It could even maintain state in an externally managed, persistent workflow engine dedicated to the service’s state transitioning logic. Security boundaries aligned with Zero Trust principles can be configured through its twin. Because the service twin is the first-class citizen in the cloud runtime environment and not the infrastructure components, all other cloud primitives and infrastructure can be scoped to it and operated as additional service capabilities. This is very powerful, and it provides only a glimpse into the potential of the Service Twin Pattern in how it could address some of the complexities of consuming cloud services.

Flipping the cloud consumption with service twins

There are direct benefits to developers for using the Service Twin Pattern. The twin enables the runtime reuse of commoditized capabilities, such as state access, pub/sub interactions, 3rd party connectors, configuration and secret access, resiliency policies, and others, as the Mecha architecture portrays. Additionally, these capabilities can be consumed as cloud capabilities scoped to a single service, and the management of the twin service can be offloaded to other teams to reduce the operational burden.

The Service Twin Pattern also has indirect second-level effects that can benefit operations teams and change how we use cloud services. In the development phase, Microservices have been embraced as the universal digital functionality unit isolated in distinct source code repositories. They are built, and outputs are stored as isolated containerized artifacts too. In the deployment phase, Microservices have also been embraced as the universal scale unit by the cloud services such as AWS SAM Stack, AWS AppRunner, and Google Cloud Run. Despite this, at runtime, when it comes to consuming other runtime primitives such as networking, storage, workflows, configurations, security, observability, etc., we currently lack a corresponding service-level representation in the cloud environments.

The Service Twin Pattern offers an approach for addressing this granularity mismatch between a service and its cloud primitives, aligning the operational concerns to the service boundary. By creating a cloud-based twin of the actual service, the service becomes a first-class entity in the cloud runtime that can serve as an independent operational management layer and a way to enhance the service with other cloud primitives. It shifts the cloud primitives to the left and forces Dev and Ops teams to think and act in a service-first manner. The service twin aligned with the developer’s view of the world also becomes the runtime management and security boundary for the operations teams.

[Click on the image to view full-size]

Flipping the cloud from infrastructure-first to service-first abstractions

This new model flips the cloud consumption from one where the infrastructure-first primitives were the primary unit of functionality into one where the application service is the primary unit, and other cloud services are complementary capabilities. It aligns a service’s development, deployment, and now runtime aspects into a single, cohesive unit, making it a powerful abstraction that reduces the cognitive load when dealing with cloud services.

Conclusion

The Service Twin Pattern is a pioneering concept that bridges the digital twin concept with microservices and the cloud. It presents a unique approach to cloud services usage, transforming how developers interact with cloud services by providing a more application-oriented and developer-friendly environment.

This pattern’s core benefits are addressing the granularity mismatch between a service and its cloud primitives, aligning the operational concerns to the service boundary, and making the service a first-class entity in the cloud runtime. Furthermore, it shifts the focus from infrastructure-first to application-first, forcing Dev and Ops teams to think and act in a service-centric manner. This shift could significantly reduce the cognitive load when dealing with cloud services, making it a powerful abstraction for developers.

However, adopting this paradigm shift in cloud service consumption has its challenges. It requires a change in mindset from both developers and operations teams, a willingness to embrace new patterns, and the readiness to adapt to a service-first approach. The real question remains whether we, as an industry, are prepared to embrace this paradigm shift and harness the full potential of cloud services.

DevOps and Cloud InfoQ Trends Report – July 2023

Key Takeaways

  • Cloud innovation has transitioned from a revolutionary phase to an evolutionary one, focusing on migrating and re-architecting workloads. The cloud space has evolved toward providing on-demand access to scalable resources and managed services, emphasizing simplifying interactions and reducing cognitive load for teams.
  • Artificial Intelligence (AI) and Large Language Models (LLMs) may play a significant role in the domains of cloud and DevOps by addressing cognitive overload and supporting tasks like instant management, ticketing systems, and code generation. Major cloud providers like Microsoft, Google, and AWS have integrated AI into their products and services, showcasing the industry’s investment in AI technology.
  • Low-code and no-code domains are impacted by AI-based and ChatGPT-like products, offering collaboration opportunities between business users and software engineering teams.
  • Platform engineering is evolving toward simplification and value delivery, adopting a platform-as-a-service mindset. The role of platform engineering teams is shifting from complex infrastructure management to becoming service providers focused on user satisfaction and value creation. Observability, financial aspects, and sustainability considerations are becoming integral to platform engineering.
  • OpenTelemetry is widely adopted for collecting metrics and event-based observability data, becoming the de facto standard in the industry. Its standardized nature encourages optimization and innovation among vendors.
  • The focus on sustainability and green computing drives architectural choices toward efficiency and minimizing carbon footprints. Site Reliability Engineering (SRE) teams are crucial in analyzing environmental impact and promoting sustainability initiatives.

The InfoQ Trends Reports provide InfoQ readers with an opinionated high-level overview of the topics we believe architects and technical leaders should pay attention to. In addition, they also help the InfoQ editorial team focus on writing news and recruiting article authors to cover innovative technologies.

In addition to this report and the updated DevOps and Cloud InfoQ trends graph, an accompanying podcast is available that features several editors and friends of InfoQ discussing these trends.

Updates on the trends graph

More details follow later in the report, but first, it is helpful to summarize the changes from last year’s trends graph.

FinOps, the practice of managing cloud costs effectively, is moving toward the early majority of adoption. The FinOps Foundation and cloud companies like Microsoft, AWS, and Google promote adopting FinOps practices, which align with sustainability goals and optimize resource usage. Recently, Google became certified as a FinOps Certified Service Provider, and Microsoft joined the FinOps organization as a premier member.

The continued evolution of WebAssembly (Wasm) is delivering on the promise toward achieving “write once, run anywhere” in the cloud, offering reusability and interoperability across different languages and platforms. eBPF (Extended Berkeley Packet Filter) is gaining traction in areas like observability and security at the kernel level.

We have observed that the concept of generic Function as a Service (FaaS) and Backend as a Service (BaaS) is gaining traction among the “late majority.” The adoption of serverless technologies and techniques has become commonplace. The statement “we are 100% serverless” no longer carries the same level of astonishment it once did, as serverless has become a mainstream approach in the industry.

Is the cloud domain moving from revolution to evolution? And is DevOps dead?

In the accompanying cloud and DevOps trends podcast discussion, the participants address the state of cloud innovation and DevOps. They agree that cloud innovation has slowed down, moving from “revolution” to “evolution”. While large numbers of organizations have adopted cloud technologies, there are many enterprises that want to migrate and re-architect workloads.

As for DevOps, it is still alive but has reached a stage of stagnancy in some organizations. The concept of DevOps, which aims to provide access and autonomy to create business value, is still alive, but the implementation has faced challenges. The panelists mentioned their interest in Value Stream management to unlock DevOps’s flow and value realization.

The public cloud vendors have evolved from their original goal of providing on-demand access to scalable resources to focus more on offering managed services. This evolution has made cloud computing more ubiquitous. However, technology is changing rapidly around existing services, new business requirements are being discovered, and new challenges are emerging. Teams must balance adopting and updating technology stacks while continually delivering business value. InfoQ Lead DevOps Editor and panelist Matthew Campbell said:

Businesses want to evolve and adapt quickly as well. […] we’re now in a phase where we’re trying to figure out how do we sustainably leverage all of the cool stuff that we’ve invented and created and all these ways of interacting with each other and move it to a place where we can innovate comfortably going forward.

In addition, cloud services are now significantly adopted throughout small and large organizations, even late adopters, and the COVID-19 pandemic was often a forcing function. For instance, the evolution of automated setup environments, such as the ability to quickly set up complete development and test environments, is now commonplace. However, challenges still exist in bridging the gap between development and operations. Identity and access management issues create a perceived boundary between the dev and ops teams.

What is the current impact of AI and LLMs on the domains of Cloud and DevOps?

The panelists discussed cognitive overload and how AI can alleviate it by addressing cognitive load limits. A specific application of AI called AIOps, which focuses on AI for IT operations, is highlighted for its effectiveness in instant management and ticketing systems. Large language Models (LLMs) have tangible benefits, such as using ChatGPT to validate information, generate teachin notes, and aid in writing and creative processes. For instance, Microsoft has integrated AI into its products and services, showcasing its significant investment in AI technology. InfoQ Lead Cloud Editor and panelist Steef-Jan Wiggers stated:

A lot of the services Microsoft offers, even something recent, the Microsoft Fabric, a complete SaaS Data Lake or Lakehouse solution they have, is thoroughly infused with AI.

Microsoft Fabric is just an example, and other public cloud providers offer AI-infused services like Amazon Sagemaker and Google’s Vertex AI and AutoML). Additional information on OpenAI can be found by following this topic on InfoQ.

How do AI-based and ChatGPT-like products impact the low-code and no-code domains?

Integrating AI into low-code tools is a business opportunity, with AI supporting business users by providing safe and valuable knowledge. This challenges previous concerns about shadow IT and encourages collaboration between product management and software engineering teams.

Furthermore, there is the idea of “ClickOps,” where low-code platforms enable users to interact through clicking while generating version-controlled, declarative, and adaptable code. For instance, improving the code generation capabilities of AI tools like GitHub Copilot and Codeium allows the generation of readable code that adheres to organizational standards and can evolve. The evolution of LLMs and AI-driven code generation will bring exciting advancements in the low-code domain.

Lastly, governance and data access in the low-code landscape is essential. It comes with challenges associated with providing business users with power and access to data while ensuring proper governance and compliance. Campbell points out that there is a need for a “DevOpsy” layer of governance within low-code augmented platform engineering that provides guardrails and prevents certain configuration boundaries being crossed.

In addition, Helen Beal, DevOps Strategic Advisor, Chief Ambassador at DevOps Institute, and panelist, added:

I found it quite interesting that we are turning a corner because AI is actually supporting businesspeople, giving them an amount of knowledge that is probably safe.

How will Platform Engineering evolve?

The evolution of platform engineering involves a shift toward simplification, focusing on value delivery, and adopting a platform-as-a-service mindset. This change entails providing self-service platforms that hide complexity and reduce cognitive load for application developers. The role of platform engineering teams is evolving from being the keepers of complex infrastructure to becoming service providers to the rest of the organization. They now focus on developer relations, marketing, and customer engagement to delight users and drive value.

Abby Bangser, a Principal Engineer at Syntasso and panelist, explains:

Platform engineering teams are learning what developer relations and marketing look like to engage with customers, get feedback, and have a roadmap that meets their needs.

Adopting technologies like Kubernetes is being pushed down the stack, with a growing emphasis on API interfaces and streamlining interactions. Additionally, there is a heightened focus on observability, including service level and key performance indicators, as well as the financial aspects of platform usage and cost justification. Overall, the future of platform engineering lies in building platforms that add value and create delightful experiences for users while addressing the evolving needs and constraints of the business.

More information on Platform Engineering can be found by following this topic on InfoQ.

Is FinOps moving to the early majority of adoption?

FinOps, managing cloud costs effectively, is moving toward the early majority of adoption. More companies are joining the FinOps Foundation, and many tools are available to support FinOps processes. However, it’s important to note that FinOps is not just about tools but also about the process and understanding of the value derived from spending. The FinOps Foundation and cloud companies like Google and Microsoft actively participate in this journey and promote adopting FinOps practices.

The awareness of FinOps is growing, leading to discussions about why certain cloud resources are being provisioned and run, and whether they are being effectively utilized. Sustainability and GreenOps are also related to FinOps, as the focus on optimizing costs aligns with the broader goal of resource efficiency. AI plays a role in FinOps by identifying unused data and helping optimize storage, contributing to financial savings and environmental benefits.

When building cloud-based applications or adopting DevOps practices, are architects and developers overloaded with security concerns?

Architects and developers face an increasing list of security concerns when building cloud-based applications or adopting DevOps practices. Developers, in particular, can feel overwhelmed by the shift left approach, where they are expected to identify and prioritize security issues throughout the development process.

While there is a growing awareness of the importance of security and a push from leadership to address these concerns, developers often need help to balance security requirements against the pressure to deliver new features.

The evolving nature of security tooling is also a factor in this landscape. Early solutions were designed by experts for experts, making them less user-friendly for developers. However, there is a growing recognition of the need for more accessible and user-friendly security tools. The goal is to make security an enablement function and build platforms that simplify security implementation while providing education and support to development teams. This approach aims to bridge the gap between expert-driven security implementations and the practical needs of developers working on the code.

Is WebAssembly (Wasm) a final realization of “write once, run anywhere” in the cloud?

Wasm is a significant step toward achieving the “write once, run anywhere” vision in the cloud. It promises reusability and interoperability, allowing developers to build libraries in one language (such as Go) and seamlessly call them from applications written in other languages that can compile down to Wasm (such as Rust).

This component model within the cloud enables the creation of applications for multiple platform targets, including ARM-based CPUs, which have gained popularity in cloud infrastructure due to their performance and cost advantages. The adoption of Wasm extends beyond application development and into cloud platform extension formats. It is used to extend cloud-native proxies, API gateways, and service meshes.

In addition to WebAssembly, eBPF is gaining traction as a platform component developer’s tool. While application engineers may not extensively use this, eBPF can be found extensively in projects that contain networking and security use cases. It allows developers to access kernel-level information and gain insights into container system operations, enhancing observability and security capabilities.

Overall, WebAssembly and eBPF offer intriguing possibilities for achieving greater portability, reusability, and performance in cloud-based applications.

More information on Wasm and eBPF can be found by following these topics on InfoQ.

How widely adopted is OpenTelemetry for collecting metrics and event-based observability data?

OpenTelemetry, a framework for collecting metrics and event-based observability data, has seen rapid adoption and is becoming the de facto standard in the industry. The collaborative efforts of many talented individuals and vendors have contributed to its cross-vendor support and cross-language compatibility, making it an essential component of applications. OpenTelemetry’s widespread adoption has been accelerated by its inclusion within major cloud vendor offerings, such as AWS (AWS Distro for OpenTelemetry), Microsoft Azure (Monitoring service), and Google Cloud Platform (Google Cloud OpenTelemetry).

The standardized nature of OpenTelemetry brings numerous benefits; it is vendor-agnostic and it has the ability to export telemetry data and utilize various tools for analysis. This standardization encourages optimization and innovation among vendors as they strive to offer unique and advanced features beyond the baseline functionalities of data collection and visualization. OpenTelemetry’s emergence as an open standard signifies the maturing of the industry and fosters healthy competition among vendors to provide compelling solutions and gain market share.

More information on OpenTelemetry can be found by following this topic on InfoQ.

What is the current state of the adoption level for Serverless?

Serverless technology has seen a shift in adoption levels where it is becoming a common choice rather than a distinct architectural concept. The term “Serverless” is less frequently used to discuss a standalone concept, as it has almost transformed into a synonym for managed services that offer scalability, micro-billing, and abstracted infrastructure. Major cloud providers like AWS, Google, and Microsoft have integrated Serverless components into their services, such as databases (DBaaS) and container runtimes (CaaS), emphasizing the benefits of auto-scaling and simplified billing structures. The focus has shifted from building architectures solely on Serverless functions to leveraging managed services, aligning with the platform engineering approach and reducing cognitive overload for developers.

The value of Serverless, such as scaling to zero and cost-per-request pricing, has found new expression beyond the traditional Serverless architecture. Organizations now recognize these benefits and are demanding them in various architectural decisions. While Serverless is one of many approaches to achieve these advantages, organizations are increasingly asking their engineering teams to deliver cost-effective solutions and optimize customer acquisition and support costs. This evolution highlights Serverless principles’ growing influence and impact on the broader architectural landscape.

More information on Serverless can be found by following this topic on InfoQ.

How is the focus on sustainability and green computing impacting cloud and DevOps?

The focus on sustainability and green computing is having a significant impact on cloud and DevOps practices. There is a growing adoption of pricing models considering the environmental impact and resource consumption of applications and services. This trend encourages organizations to make architectural choices that prioritize efficiency and sustainability. Managed services are favored as they offer optimized resource utilization and scalability, allowing businesses to minimize their carbon footprint and reduce energy consumption. The consideration of pricing about architecture and the adoption of managed services aligns with sustainability and green computing goals.

Regarding responsibility, there is a recognition that addressing sustainability falls within the realm of Site Reliability Engineering (SRE) and related roles. These teams are well-positioned to analyze the environmental impact of technology decisions and drive initiatives that promote efficiency and sustainability. Architecture discussions now encompass considerations such as componentization, isolation, security, and cost-efficiency. Organizations are evaluating their requirements and seeking middle-ground solutions that meet security needs without unnecessarily high costs. This reflects a shift toward more pragmatic approaches to security, finding the right balance between enterprise-grade features and cost-effectiveness.

What are our predictions for the future of the cloud and DevOps spaces?

The panelist’ predictions for the cloud and DevOps spaces’ future revolved around simplifying, reducing cognitive overload, and focusing on innovation. There is a desire to streamline processes and tools to allow teams to concentrate on their specific areas of expertise and maximize their impact.

The convergence of AIOps, platform engineering, sustainability, and FinOps is a positive shift that may lead to more focused, more effective, and happier teams. The challenge lies in distinguishing between hype and genuine opportunities, acknowledging the “nuggets of value” within emerging trends while remaining critical of the “overselling” and broad applicability claims.

Open-source adoption, the standardization facilitated by initiatives like OpenTelemetry and CloudEvents, and the potential of AI-infused services such as Copilots and ChatGPT are all points of excitement. Overall, there is a sense of enthusiasm for the ongoing developments and the learning opportunities they bring.

TimeProvider and ITimer: Writing Unit Tests with Time in .NET 8 Preview 4

Key Takeaways

  • Handling date and time correctly in .NET can be a challenging task.

  • DateTimeOffset, with stored UTC offset and time zone, provides more accuracy for date and time storage than the DateTime structure.

  • Although you can mock external structures like DateTime and DateTimeOffset in unit tests with .NET Fakes, this feature is only available in the Enterprise version of Visual Studio.

  • In .NET 8 Preview 4, Microsoft introduced TimeProvider and ITimer as universal time abstractions for dependency injections and unit testing.

  • TimeProvider is overloaded with properties and methods, providing extensive functionality for managing time-related operations.

Time plays a critical role in software applications. Tracking time zones and testing time-dependent flows bring challenges to developers. The first part of the article covers the history of .NET Date and Time structures, including existing issues and challenges in time calculation. The second part reviews new .NET 8 Preview 4 abstractions that improve dependency injection and unit testing.

Challenges in utilizing old date and time structures in .NET

DateTime

DateTime has been the main structure for storing date and time in .NET since version 1.1. It has a major drawback – a lack of a time zone. To overcome this problem, the Kind property was added with three possible values: Local, Utc, or Unspecified.

By default, DateTime.Now initializes the local time instance with the Kind property equal to Local.

       var now = DateTime.Now;
       Console.WriteLine("Now: {0}", now);
       Console.WriteLine("Kind: {0}", now.Kind);

Output:

Now: 06/15/2023 11:00:00
Kind: Local

For the conversion to UTC, it is possible to call the DateTime.Now.ToUniversalTime() method, or simply use the DateTime.UtcNow property.

       var now = DateTime.Now;
       var utc1 = now.ToUniversalTime();
       Console.WriteLine("UTC 1: {0}",utc1);
       Console.WriteLine("UTC 1 Kind: {0}", utc1.Kind);
		
       var utc2 = DateTime.UtcNow;
       Console.WriteLine("UTC 2: {0}", utc2);
       Console.WriteLine("UTC 2 Kind: {0}", utc2.Kind);

Output:

UTC 1: 06/15/2023 11:00:00
UTC 1 Kind: Utc
UTC 2: 06/15/2023 11:00:00
UTC 2 Kind: Utc

How does .NET understand the time zone difference during translation from local time to UTC if DateTime does not provide this information?

The ToUniversalTime method takes the time zone from the operating system, an approach that might be problematic. Let’s consider a local DateTime.Now instance that is created in a New York server and then transferred to a London server.

After the conversion to UTC simultaneously on both servers, the results will be different:

      var nowInNewYork = DateTime.Now;
	  var utcInNewYork = nowInNewYork.ToUniversalTime();		
	  Console.WriteLine("UTC in New York: {0}", utcInNewYork);

      // Send locally initiated nowInNewYork to a London server
	  var utcInLondon = nowInNewYork.ToUniversalTime();		
      Console.WriteLine("UTC in London:   {0}", utcInLondon);

Output:

UTC in New York: 06/15/2023 11:00:00
UTC in London:   06/15/2023 07:00:00

This approach led to confusion and frustration for developers; the problems were described in the Microsoft blog along with endless Stackoverflow questions.

On this occasion, Microsoft released the Coding Best Practices Using DateTime in the .NET Framework, shifting all responsibility to the developers:

A developer is responsible for keeping track of time-zone information associated with a DateTime value via some external mechanism. Typically this is accomplished by defining another field or variable that you use to record time-zone information when you store a DateTime value type.

.NET expects time-zone information to be paired with the Kind property during date and time restoration: DateTimeKind.Local with TimeZoneInfo.Local, DateTimeKind.Utc with TimeZoneInfo.Utc, and DateTimeKind.Unspecified for anything else. Custom time zone, as Microsoft recommends, requires to use DateTimeKind.Unspecified:


       var nowInNewYork = DateTime.Now;   
	   Console.WriteLine("New York local time: {0}", nowInNewYork);

      var newYorkTimeZone =
 TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
	  nowInNewYork = DateTime.SpecifyKind(nowInNewYork, DateTimeKind.Unspecified);

       // Now on any server, in New York or London, UTC is correct
       var utc = TimeZoneInfo.ConvertTimeToUtc(nowInNewYork, newYorkTimeZone)
	   Console.WriteLine("UTC: {0}", utc);

Output:

New York local time: 06/15/2023 12:00:00
UTC: 06/15/2023 16:00:00

An alternative solution may be to create and store time only in the UTC format:

var dtInUtc = DateTime.UtcNow;

then convert it every time to a local time on the user’s side:

var dtLocal = dtInUtc.ToLocalTime();
// or
var dtLocal = TimeZoneInfo.ConvertTimeToUtc(dtInUtc, clientTimeZone);

Unfortunately, this requires additional code checks against accidental use of DateTime with a local initialization via DateTime.Now, and also does not exclude such nuances as daylight saving rule changes. I will discuss it in the next section.

DateTimeOffset

As an improvement, the DateTimeOffset structure was introduced in .NET 2. It consists of:

  • structure DateTime
  • property Offset storing time difference relative to UTC.

The problem with servers in different time zones will not affect DateTimeOffset.Now – this is not a panacea for all cases.

Assume there is a need to save an April appointment with a doctor in London. The time zone for London in .NET is TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time"), passing it to DateTimeOffset will result in a correct instance, for example, equal to 2023-04-01 14:00:00 with Offset = +1. Due to daylight saving in Britain, Offset equals +1 at the end of March and equals 0 at the beginning of March. In some countries, there is no daylight saving time. For example, in Nigeria, Offset is always equal to +1.

The daylight saving rules are subject to change, with alternation happening almost every year. It will not be possible to determine the origin of date and time just by looking at the Offset value, whether it was created for Britain or Nigeria with the same value “01:00:00”:

 var dt = DateTimeOffset.Now; 
 Console.WriteLine(dt.Offset);

Output:

01:00:00

Thus, for international software, it is better to store the time zone for possible recalculation of Offset according to the new rules; TimeZoneInfo class suits this requirement.

Writing unit tests with time before .NET 8 Preview 4

For unit testing, it is important to be able to mock a method call on an object with a custom implementation. As DateTime and DateTimeOffset do not have interfaces, it is possible to create a custom abstraction and later mock them during tests. For example, the following interface can provide an abstraction for DateTimeOffset:

public interface ISystemClock
{
    DateTimeOffset UtcNow { get; }
}

A similar approach was used internally at Microsoft where the same code was added to at least four different areas in .NET.

What about alternative solutions?

Jon Skeet created the NodaTime library with the correct time processing of non-trivial cases, and of course, with the support of abstractions.

Alright, using some custom interface for your code base is possible. Though how can you create integration tests with external libraries that require time transfer via DateTime and DateTimeOffset types?

Microsoft has a tool called .NET Fakes that can generate mocks and stubs for any .NET library. For example, it is possible to overwrite all static calls of DateTime.Now in unit tests with

System.Fakes.ShimDateTime.NowGet = () => { return new 
DateTime(2025, 12, 31); };

It works, but there are limitations.

First, the generator is compatible only with Windows OS. Second, at the time of writing, it is included only in the Visual Studio Enterprise version, priced at $250 per month. Third, it is a complex and the most advanced integrated development environment tool from Microsoft. It takes a lot of resources and local storage space, compared to the lightweight Visual Studio Code IDE.

Writing unit tests with time in .NET 8 Preview 4

The long-awaited time abstractions were added after years of debates and hundreds of comments for .NET 8 RC: TimeProvider and ITimer.

public abstract class TimeProvider
{
    public static TimeProvider System { get; }
    protected TimeProvider()
    public virtual DateTimeOffset GetUtcNow()
    public DateTimeOffset GetLocalNow()
    public virtual TimeZoneInfo LocalTimeZone { get; }
    public virtual long TimestampFrequency { get; }
    public virtual long GetTimestamp()
    public TimeSpan GetElapsedTime(long startingTimestamp)
    public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)
    public virtual ITimer CreateTimer(TimerCallback callback, object? state,TimeSpan dueTime, TimeSpan period)
}

public interface ITimer : IDisposable, IAsyncDisposable
{
    bool Change(TimeSpan dueTime, TimeSpan period);
}


Ultimately, it did not turn out to be flawless, but this represents significant progress.

TimeProvider disadvantages

1. The abstract class came out bulky. For a method that takes a TimeProvider argument, it is not possible to decide which method to mock without knowing internal details: GetUtcNow(), GetLocalNow(), CreateTimer(...), or all of them. Developers proposed to break the new type into small interfaces, in particular, similar to the one already used internally at Microsoft:

public interface ISystemClock
{
    public DateTimeOffset GetUtcNow();
}

public abstract class TimeProvider: ISystemClock {
    // ...
}

That idea had been rejected.

2. An instance of TimeProvider can be created with the help of a static TimeProvider.System call. It is very easy to use for a developer, though it is not so different from the old usage of static DateTime.Now. Later it will result in problems with unit test writing without a special FakeTimeProvider.
Instead of a direct static call, it is expected that developers will use Dependency Injection for TimeProvider. For example, code for ASP.NET Core can look like

public class MyService
{
    public readonly TimeProvider _timeProvider;

    public MyService(TimeProvider timeProvider){
        _timeProvider = timeProvider;
    }

    public boolean IsMonday() {
        return _timeProvider.GetLocalNow().DayOfWeek == DayOfWeek.Monday;   
    }         
}

// Dependency injection:
var builder = WebApplication.CreateBuilder();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton();

This can be non-trivial for beginner programmers.

TimeProvider and ITimer advantages

1. Time-dependent unit testing becomes more universal. TimeProvider was added to BCL to support a wide variety of .NET runtimes. It becomes possible to cover the above MyService example with a unit test:

using Moq;
using NUnit.Framework;

[Test]
public void MyTest()
{
    var mock = new Mock();
 mock.Setup(x => x.GetLocalNow()).Returns(new DateTimeOffset(2025, 12, 31, 23, 59, 59, TimeSpan.Zero));
    var mockedTimeProvider = mock.Object;

    var myService = new MyService(mockedTimeProvider);
    var result = myService.IsMonday(mockedTimeProvider);
    Assert.IsTrue(result, "Should be Monday");
}

2. The Microsoft team did not bring in the new implementation of the old error where properties had side effects. This is how DateTime.Now property was mistakenly introduced instead of the DateTime.Now() function. TimeProvider abstracts time side effects with functions and methods: GetUtcNow(), GetLocalNow(), GetTimestamp(), etc.

3. It is possible to test time series events with TimeProvider.CreateTimer(...) and Timer.Change(...) functions. This is especially important for Task.Delay(...) and Task.WaitAsync(...)function calls, which now also accept a TimeProvider argument.

4. There is a plan to create FakeTimeProvider as part of .NET to further simplify unit testing. Perhaps then the negative point 2 will not be relevant.

Stephen Toub, a software engineer at Microsoft, wrote:

At the end of the day, we expect almost no one will use anything other than TimeProvider.System in production usage. Unlike many abstractions then, this one is special: it exists purely for testability.

Conclusion

The introduction of the class TimeProvider in .NET 8 Preview 4 specifies a standardized and unified abstraction for managing time. While it may have a few minor drawbacks, Microsoft teams internally marked their custom time interfaces as obsolete and now advocate for adopting TimeProvider.

Creating Your Own AI Co-author Using C++

Key Takeaways

  • The use of MS Office automation and the ChatGPT API can greatly enhance the editing process by automating the extraction and incorporation of editorial comments, as well as leveraging AI-based suggestions for improving the text.
  • We can build a tool in pure C++ that automates the editing workflow, while scanning and extracting editorial comments from a Word file, storing them in a database, and using ChatGPT to generate targeted questions for improving the text.
  • Enumerating comments allows us to not only retrieve the comment text but also the associated text segment, providing the necessary context for understanding the purpose of each comment.
  • When working with web APIs, we rely on a versatile code structure that enables us to send requests and handle responses using the JSON data format. To facilitate this process, we utilize libCurl, a robust tool widely utilized for data transfer across various networks.
  • Several building blocks are used to accomplish the task: ChatGPT API, a generic function, OLE automation, as well as several other components.

While using ChatGPT through a web interface is one thing, creating your own autonomous AI tool that interfaces with ChatGPT via its API, is a different story altogether – especially when you aim to maintain complete control over the interaction with the user. At the same time, as strong proponents of C++, we believe that a GPT tool in C++ will ease the pain of dealing with the daunting task of editing (endless) editorial comments.

General idea

We aim to explore the realm of MS Office automation and leverage the ChatGPT API to enhance the editing process. We envision a sophisticated tool that seamlessly integrates C++ with the ChatGPT API, providing a new way to interact with editorial comments in Word documents.

Traditional document editing involves manually reviewing content and adding comments to specific sections. In our case, as we worked on our C++ book, we encountered over 100 editorial comments each time, most of which related to the publisher’s style guide and annotations. It would have been helpful to have a way to store these comments and associated text in a database, not to mention the potential for AI-based editing. That’s precisely what our software accomplishes: by automating this process, we can expedite the editing workflow. While this tool serves as proof of concept (POC) and is not recommended for writing and editing entire books, it still presents an exciting exercise in automation and is certainly worth trying.

How it’s done

The workflow begins with our software scanning the Word file, meticulously examining each editorial comment embedded within the document using Office Automation API.

Once enumerated all comments, our tool extracts them along with the associated text segments and stores them in a sqlite3 database. Based on this, it prepares targeted questions for ChatGPT revolving around how to improve or fix a particular section of text. By leveraging the ChatGPT API, we can tap into the language model’s vast knowledge and linguistic prowess to obtain expert suggestions and recommendations.

Upon receiving a response from ChatGPT, our tool dynamically incorporates the suggested edits into the associated text segments, seamlessly enhancing the content based on the model’s insights.

This automated editing process significantly reduces manual effort and accelerates overall document refinement. Our tool even tracks the changes but remembers to turn ‘track changes’ off, when done.

Programming-wise, there are several building blocks in our project, and some of them can be expanded or replaced to serve different purposes. Let’s call our code Proof of Concept.

The building blocks

Here are the players involved in the process – our building blocks:

Chat GPT API

Our tool interfaces and interacts with ChatGPT by utilizing various parameters and approaches. We prepare payloads to be sent to the API and parse the responses. To use our tool, you must obtain an API key and add it to our code instead of ““. Here is a code snippet demonstrating the basics of interfacing with ChatGPT.

The advantage of using the API includes being able to interface and interact with Chat GPT, using different parameters and approaches, preparing payloads to be sent to the API, and parsing the response we get back.

When using ChatGPT API, there are several things to take into consideration.

Our Generic Function

For the purpose of this article, we created a generic function. That function is modular as it generates requests with modular attributes and parameters in the following form:

data =
{
	{"messages", json::array({{ {"role", "user"}, {"content", entire_converstaion_string} }})},
	{"model", model},
	{"temperature", temperature},
	{"max_tokens", max_tokens},
	{"n", n},
	{"stop", stop}
};

Let’s go over some issues and requirements along with these attributes:

  • messages“- defines the conversation history between the user and the model. Each message in the conversation consists of two properties: “role” (which can be “system”, “user”, or “assistant”) and “content” (the actual text of the message). For the purpose of this article, we used “user
  • model” – allows you to specify which version of the ChatGPT model you want to use. for the purpose of this article we used “gpt-3.5-turbo
  • temperature” – can be set to control the level of similarity between the generated text and the prompt. For example, a high-temperature value can be used to generate text that is more different from the prompt, while a low-temperature value can be used to generate text that is more similar to the prompt. This can be useful in situations where the goal is to generate text that is similar to a given input but with some level of variation or “creativity.”
  • max_tokens” – is the maximum number of tokens to be used for each request. The number of tokens processed depends on the length of the input and output text.
    • 1-2 sentences ~= 30 tokens
    • 1 paragraph ~= 100 tokens
    • 1,500 words ~= 2048 tokens

As a user of ChatGPT API, you will be charged for the tokens you consume.

Model

Price for 1000 tokens (prompt)

Price for 1000 tokens (completion)

ChatGPT $0.0020     $0.0020
  • n” controls how many responses the model should provide; it is set to one, a single response, by default.
  • stop” indicates the string that should trigger the model to stop generating its response. Set to newline by default. This means that when the model encounters a new line in its output, it will stop generating after that.

Our Prompt

We always like to say that the significance of well-structured prompts cannot be overstated. A carefully constructed prompt acts as a guiding blueprint, influencing the quality of the generated output. In this article, we will delve into the components of an effective prompt and offer practical examples and guidelines to help C++ students maximize the potential of ChatGPT API in their projects.

Here is an example:

// Setting up a prompt for GPT request
wstring prompt{ L"I will send you some text, and an associated comment that tells what changes need to be made in the text. Please start your response with 'Changed text: ' followed by the actual updated text. Here is the original text: '" };
prompt += rangeText;
prompt += L"'. And here is the associated comment suggesting the change: '";
prompt += commentText;
prompt += L"'. Please do not respond with anything else, only include the changed text and nothing else. If you do not have the correct answer or don't know what to say, respond with these exact words: 'I do not understand";
//

When you compose a prompt, it is best to create a template containing the constant parts of the requests you will use throughout the program and then change the variable parts based on the immediate need. Here are some key building blocks for a good prompt:

Context:

Context serves as the groundwork for the prompt, offering crucial background information. It enables the Language Model to grasp the task’s essence. Whether it entails a concise problem description or a summary of pertinent details, providing context is pivotal.

Example:
“You are a software developer working on a mobile app for a food delivery service. The app aims to provide a seamless experience for users to order food from local restaurants. As part of the development process, you need assistance generating engaging and informative content about the app’s features.”

Task:
The task defines the precise goal or objective of the prompt. It should be clear, concise, and focus on the specific information or action expected from the ChatGPT model.

Example:
“Compose a short paragraph that highlights the app’s key features and showcases how they enhance the food delivery experience for customers.”

Constraints:
Constraints set boundaries or limitations for the prompt. They may encompass specific requirements, restrictions on response length or complexity, or any other pertinent constraints. By defining constraints, you can guide the generated output toward the desired outcome.

Example:
“The response should be concise, with a maximum word count of 150 words. Focus on the most prominent features that differentiate the app from competitors and make it user-friendly.”

Additional Instructions:
In this section, you have the opportunity to provide supplementary context or specify the desired output format. This can include details regarding the expected input format or requesting the output in a specific format, such as Markdown or JSON.

Example:
“Please format the response as a JSON object, containing key-value pairs for each feature description. Each key should represent a feature, and its corresponding value should provide a brief description highlighting its benefits.”

By understanding and implementing these fundamental components, C++ developers can master the art of constructing effective prompts for optimal utilization of the ChatGPT API in their projects. Thoughtfully incorporating context, defining clear tasks, setting constraints, and providing additional instructions will enable developers to achieve precise and high-quality results.

Continuous chat

In most cases, we would like to be able to continue a conversation from where you left off last time. There is a special flag used by Chat GPT API to allow that. If it isn’t set, here is what will happen:

➢    What is the capital of France?

Request payload: '{"messages":[{"content":"what is the capital of france?","role":"user"}],"model":"gpt-3.5-turbo"}'
Callback JSON: '{"id":"chatcmpl-7AlP3bJX2T7ibomyderKHwT7fQkcN","object":"chat.completion","created":1682799853,"model":"gpt-3.5-turbo-0301","usage":{"prompt_tokens":15,"completion_tokens":7,"total_tokens":22},"choices":[{"message":{"role":"assistant","content":"The capital of France is Paris."},"finish_reason":"stop","index":0}]}

Your AI friend responds:

➢    The capital of France is Paris.

Then comes a follow-up question:

➢    How big is it?

Request payload: '{"messages":[{"content":"How big is it?","role":"user"}],"model":"gpt-3.5-turbo"}'
Callback JSON: '{"id":"chatcmpl-7AlPAabscfyDrAV2wTjep0ziiseEB","object":"chat.completion","created":1682799860,"model":"gpt-3.5-turbo-0301","usage":{"prompt_tokens":13,"completion_tokens":20,"total_tokens":33},"choices":[{"message":{"role":"assistant","content":"I apologize, but I need more context to accurately answer your question. What are you referring to?"},"finish_reason":"stop","index":0}]}


➢    I apologize, but I need more context to accurately answer your question. What are you referring to?

To fix that, we need to maintain a continuous chat, but how do we do that? In fact, the only way to do that must include passing back and forth a string containing the entire conversation.

    string entire_converstaion_string;

We also define:

	using Conversation = vector;

which is defined as

	using SingleExchange = pair;

In our source code, you can see how we maintain our Conversation object up to a fixed length (as, clearly, we can’t store endless conversations). This fixed length is set here:

	int conversation_exchange_limit{ 100 };

As already mentioned, our prompt plays a key role in the efficiency of the request, and when it comes to continuous chats, we may want to use a different prompt:

	string prompt_start{ "You are a chatbot. I want to have a conversation 
with you where you can remember the context between multiple requests. To do 
that, I will send all previous request prompts and your corresponding 
responses, please use those as context. All the previous request prompts and 
the current will have the 'request: ' before it, and all your corresponding 
responses will have 'response: ' before it. Your job is to respond to only the 
latest request. Do not add anything by yourself, just respond to the latest 
request. Starting nownn" };

Multi-part response

When you ask your AI friend:

➢    Write me a C++ code that counts from 1 to 10

You may get just that:

➢    Sure, here’s the C++ code to count from 1 to 10:

Without any source code.

Here is why: The stop parameter sent to the API lets the model know at what point of its output it should stop generating more. The newline is the default when nothing is specified, and it means that the model stops generating more output after the first newline it outputs.

But if you set the “stop” parameter to an empty string, you will get the full response including the source code:

[Click on the image to view full-size]

About OLE Automation

OLE Automation is a technology introduced by Microsoft in the past that has since evolved. In our implementation, we utilize Microsoft automation directly, bypassing the use of MFC (Microsoft Foundation Classes). To access various elements of MS Word, such as documents, active documents, comments, etc., we define an IDispatch COM interface for each object we need to interact with.

Office Automation

Our tool automates various tasks and features within MS Word. It can read comments, find associated text, turn on/off “Track Changes,” work in the background, replace text, add comments, save the result, and close the document. Here is a description of the functions we use:

OLEMethod(): A helper function that invokes a method on an IDispatch interface, handling method invocations and returning HRESULT values indicating errors.

Initialize(): A function that initializes the OfficeAutomation class by creating an instance of the Word application and setting its visibility. It initializes the COM library, retrieves the CLSID for the Word application, creates an instance of the application, and sets its visibility.

OfficeAutomation(): The constructor of the OfficeAutomation class. It initializes member variables and calls the Initialize function with false to create a non-visible Word application instance.

~OfficeAutomation(): The destructor of the OfficeAutomation class. It does nothing in this implementation.

SetVisible(): A function that sets the visibility of the active document. It takes a boolean parameter to determine whether the document should be visible or not. It uses the OLEMethod function to set the visibility property of the Word application.

OpenDocument(): A function that opens a Word document and sets its visibility. It takes a path to the document and a boolean parameter for visibility. It initializes the class if necessary, retrieves the Documents interface, opens the specified document, and sets its visibility.

CloseActiveDocument(): A function that closes the active document. It saves the document and then closes it. It uses the OLEMethod function to call the appropriate methods.

ToggleTrackChanges(): A function that toggles the “Track Revisions” feature of the active document. It gets the current status of the feature and toggles it if necessary. It uses the OLEMethod function to access and modify the “TrackRevisions” property.

FindCommentsAndReply(): A function that finds all comments in the active document, sends a request to the ChatGPT API for suggestions, and updates the associated text of each comment based on the API response. It iterates through each comment, retrieves the associated text range, sends a prompt to the ChatGPT API with the text and comment as context, receives the API response, and updates the text range with the suggested changes.

CountDocuments(): A function that returns the number of open documents in the Word application associated with the OfficeAutomation class. It retrieves the Documents interface and returns the count.

Handling comments

When developing a mechanism that will go over comments, we need to be able to enumerate all comments and distinguish between resolved ones and non-resolved ones.

That is done the following way:

bool IsCommentResolved(IDispatch* pComment)
{
        // Check if the comment is resolved
        VARIANT isResolved;
        VariantInit(&isResolved);
        HRESULT hr = OLEMethod(DISPATCH_PROPERTYGET, &isResolved, pComment, 			(LPOLESTR)L"Done", 0);


    if (FAILED(hr)) 
    {
        ShowError(hr);
        return false;
    }
    bool resolved = (isResolved.vt == VT_BOOL && isResolved.boolVal == VARIANT_TRUE);
    return resolved;
}

As you can see, using OLEMethod() along with DISPATCH_PROPERTYGET, allows us to check the property name “Done” which will indicate resolved comments.

Enumerating comments

Next, we can just enumerate all comments in the document, and maybe print the “Resolved” status per each of these comments.

Before we start, we would want to not just enumerate the comments, but also the text associated with them. The reason for that is laid on the initial purpose of commenting. The author of a document composes and edits the document. The editor marks a segment, which can be a paragraph, sentence, or even a word, and adds a comment. When we read a comment, we need the context of that comment, and the context would be that marked segment.

So when we enumerate all comments, we do not just print the comment’s text but also the text associated with it (our segment).

When we start going over all comments, we need to declare and initialize 2 pointers:

pComments – points to the document’s comments.
pRange – points to the document’s content (the segment that holds the text associated with the comment).

Each of them is initialized:

{
	VARIANT result;
	VariantInit(&result);
	m_hr = OLEMethod(DISPATCH_PROPERTYGET, &result, m_pActiveDocument, (LPOLESTR)L"Comments", 0);
	if (FAILED(m_hr))
	{
		ShowError(m_hr);
		return m_hr;
	}


	pComments = result.pdispVal;
}


{
	VARIANT result;
	VariantInit(&result);
	m_hr = OLEMethod(DISPATCH_PROPERTYGET, &result, m_pActiveDocument, (LPOLESTR)L"Content", 0);
	if (FAILED(m_hr))
	{
		ShowError(m_hr);
		return m_hr;
	}
	pRange = result.pdispVal;
}

Then we can start our loop to iterate through all comments in the document.

You can see how that’s done in our source code, but generally speaking, we start with the comment, go to the associated text, and check if the comment is resolved. Then we can either print it to a report, add it to a database, or send it to Chat GPT API.

General Code for API Interfacing

To interface with any API over the web, we employ general code that facilitates sending requests and parsing responses using the JSON data format. In this process, we utilize libCurl, a powerful tool widely used for transferring data across the network using the command line or scripts. It has extensive applications across different domains, including automobiles, televisions, routers, printers, audio equipment, mobile devices, set-top boxes, and media players. It serves as the internet transfer engine for numerous software applications, with billions of installations.

If you check our source code, you can see how libCurl is used.

To sum up

By utilizing the power of MS Office automation and integrating it with the ChatGPT API, we empower editors and writers to streamline their workflow, saving valuable time and improving the quality of their work. The synergy between C++ and the ChatGPT API facilitates smooth and efficient interaction, enabling our tool to provide intelligent and context-aware recommendations for each editorial comment.

As a result, our small MS Office automation POC tool, powered by the ChatGPT API and C++, revolutionizes the editing process. By automating the extraction of editorial comments, interacting with ChatGPT to seek expert guidance, and seamlessly integrating the suggested edits, we empower users to enhance the quality and efficiency of their work in Word documents. This powerful combination of technologies opens new possibilities for efficient document editing and represents a significant leap forward in the field of MS Office automation.

A Comprehensive Guide to Java’s New Feature: Pattern Matching for Switch

Key Takeaways

  • Pattern matching for the switch control-flow statement is a new feature introduced in Java 17 and refined in subsequent versions.
  • A pattern can be used in case labels as case p. The selector expression is evaluated, and the resulting value is tested against the case labels that may include patterns. The execution path of the first matching case label applies to the switch statement/expression.
  • Pattern matching adds support for a selector expression of any reference type in addition to the existing legacy types.
  • Guarded patterns can be used with the new when clause in a case label pattern.
  • Pattern matching can be used with the traditional switch statements and with the traditional fall-through semantics of a switch statement.

A switch statement is a control-flow statement that was originally designed to be a short-form alternative to the if-else if-else control-flow statement for certain use cases that involved multiple possible execution paths based on what a given expression evaluates to.

A switch statement consists of a selector expression and a switch block consisting of case labels; the selector expression is evaluated, and the execution path is switched based on which case label matches the result of the evaluation.

Originally switch could only be used as a statement with the traditional case ...: label syntax with fall-through semantics. Java 14 added support for the new case ...-> label syntax with no fall-through semantics.

Java 14 also added support for switch expressions. A switch expression evaluates to a single value. A yield statement was introduced to yield a value explicitly.

Support for switch expressions, which is discussed in detail in another article, means that switch can be used in instances that expect an expression such as an assignment statement.

Problem

However, even with the enhancements in Java 14, the switch still has some limitations:

  1. The selector expression of switch supports only specific types, namely integral primitive data types byte, short, char, and int; the corresponding boxed forms Byte, Short, Character, and Integer; the String class; and the enumerated types.
  2. The result of the switch selector expression can be tested only for exact equality against constants. Matching a case label with a constant test only against one value.
  3. The null value is not handled like any other value.
  4. Error handling is not uniform.
  5. The use of enums is not well-scoped.

Solution

A convenient solution has been proposed and implemented to counter these limitations: pattern matching for switch statements and expressions. This solution addresses all the issues mentioned above.

Pattern matching for the switch was introduced in JDK 17, refined in JDK 18, 19, and 20, and is to be finalized in JDK 21.

Pattern matching overcomes the limitations of the traditional switch in several ways:

  1. The type of the selector expression can be any reference type in addition to an integral primitive type (excluding long).
  2. Case labels can include patterns in addition to constants. A pattern case label can apply to many values, unlike a constant case label that applies to only one value. A new case label, case p, is introduced in which p is a pattern.
  3. Case labels can include null.
  4. An optional when clause can follow a case label for conditional or guarded pattern matching. A case label with a when is called a guarded case label.
  5. Enum constant case labels can be qualified. The selector expression doesn’t have to be an enum type when using enum constants when using enum constants.
  6. The MatchException is introduced for a more uniform error handling in pattern matching.
  7. The traditional switch statements and the traditional fall-through semantics also support pattern matching.

A benefit of pattern matching is to facilitate data oriented programming, such as improving the performance of complex data-oriented queries.

What is pattern matching?

Pattern matching is a powerful feature that extends the functionality of control-flow structures in programming. This feature allows a selector expression to be tested against several patterns in addition to the test against traditionally supported constants. The semantics of the switch stays unchanged; a switch selector expression value is tested against case labels that may include patterns, and if the selector expression value matches a case label pattern, that case label applies to the execution path of the switch control-flow. The only enhancement is that the selector expression can be any reference type in addition to primitive integral types (excluding long). The case labels can include patterns in addition to constants. Additionally, supporting null and qualified enum constants in case labels is an added feature.

The grammar of switch labels in a switch block is as follows:

SwitchLabel:
  case CaseConstant { , CaseConstant }
  case null [, default]
  case Pattern
  default

Pattern matching can be used with the traditional case …: label syntax with fall-through semantics, and with the case … -> label syntax with no fall-through semantics. Nonetheless, it’s essential to note that a switch block cannot mix the two types of case labels.

With these modifications in place, pattern matching has paved the way for more sophisticated control-flow structures, transforming the richer way to approach logic in code.

Setting the environment

The only prerequisite to running the code samples in this article is to install Java 20 or Java 21 (if available). Java 21 makes only one enhancement over Java 20, which is support for qualified enum constants in case labels. The Java version can be found with the following command:

java --version
java version "20.0.1" 2023-04-18
Java(TM) SE Runtime Environment (build 20.0.1+9-29)
Java HotSpot(TM) 64-Bit Server VM (build 20.0.1+9-29, mixed mode, sharing)

Because switch pattern matching is a preview feature in Java 20, javac and java commands must be run with the following syntax:

javac --enable-preview --release 20 SampleClass.java
java --enable-preview  SampleClass

However, one can directly run this using the source code launcher. In that case, the command line would be:

java --source 20 --enable-preview Main.java

The jshell option is also available but requires enabling the preview feature as well:

jshell --enable-preview

A simple example of pattern matching

We start with a simple example of pattern matching in which the selector expression type of a switch expression is reference type; Collection; and the case labels include patterns of the form case p.  

import java.util.Collection;
import java.util.LinkedList;
import java.util.Stack;
import java.util.Vector;

public class SampleClass {
    static Object get(Collection c) {

        return switch (c) {
            case Stack s -> s.pop();
            case LinkedList l -> l.getFirst();
            case Vector v -> v.lastElement();
            default -> c;
        };
    }

    public static void main(String[] argv) {

        var stack = new Stack();
        stack.push("firstStackItemAdded");
        stack.push("secondStackItemAdded");
        stack.push("thirdStackItemAdded");

        var linkedList = new LinkedList();

        linkedList.add("firstLinkedListElementAdded");
        linkedList.add("secondLinkedListElementAdded");
        linkedList.add("thirdLinkedListElementAdded");

        var vector = new Vector();

        vector.add("firstVectorElementAdded");
        vector.add("secondVectorElementAdded");
        vector.add("thirdVectorElementAdded");

        System.out.println(get(stack));
        System.out.println(get(linkedList));
        System.out.println(get(vector));
    }
}

Compile and run the Java application, with the output:

thirdStackItemAdded
firstLinkedListElementAdded
thirdVectorElementAdded

Pattern matching supports all reference types

In the example given earlier, the Collection class type is used as the selector expression type. However, any reference type can serve as the selector expression type. Therefore, the case label patterns can be of any reference type compatible with the selector expression value. For example, the following modified SampleClass uses an Object type selector expression and includes case label patterns for a record pattern and an array reference type pattern, in addition to the case label patterns for previously used Stack, LinkedList, and Vector reference types.

import java.util.LinkedList;
import java.util.Stack;
import java.util.Vector;

record CollectionType(Stack s, Vector v, LinkedList l) {
}

public class SampleClass {
    static Object get(Object c) {
        return switch (c) {
            case CollectionType r -> r.toString();
            case String[] arr -> arr.length;
            case Stack s -> s.pop();
            case LinkedList l -> l.getFirst();
            case Vector v -> v.lastElement();
            default -> c;
        };
    }

    public static void main(String[] argv) {

        var stack = new Stack();
        stack.push("firstStackItemAdded");
        stack.push("secondStackItemAdded");
        stack.push("thirdStackItemAdded");

        var linkedList = new LinkedList();

        linkedList.add("firstLinkedListElementAdded");
        linkedList.add("secondLinkedListElementAdded");
        linkedList.add("thirdLinkedListElementAdded");

        var vector = new Vector();

        vector.add("firstVectorElementAdded");
        vector.add("secondVectorElementAdded");
        vector.add("thirdVectorElementAdded");

        var r = new CollectionType(stack, vector, linkedList);
        System.out.println(get(r));
        String[] stringArray = {"a", "b", "c"};

        System.out.println(get(stringArray));
        System.out.println(get(stack));
        System.out.println(get(linkedList));
        System.out.println(get(vector));

    }
}

This time the output is as follows:

CollectionType[s=[firstStackItemAdded, secondStackItemAdded, thirdStackItemAdded
], v=[firstVectorElementAdded, secondVectorElementAdded, thirdVectorElementAdded
], l=[firstLinkedListElementAdded, secondLinkedListElementAdded, thirdLinkedList
ElementAdded]]
3
thirdStackItemAdded
firstLinkedListElementAdded
thirdVectorElementAdded

The null case label

Traditionally, a switch throws a NullPointerException at runtime if the selector expression evaluates to null. A null selector expression is not a compile-time issue. The following simple application with a match-all case label default demonstrates how a null selector expression throws a NullPointerException at runtime.

import java.util.Collection;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            default -> c;
        };
    }

    public static void main(String[] argv) {
        get(null);
    }
}

It is possible to test a null value explicitly outside the switch block and invoke a switch only if non-null, but that involves adding if-else code. Java has added support for the case null in the new pattern matching feature. The switch statement in the following application uses the case null to test the selector expression against null.

import java.util.Collection;

public class SampleClass {
    static void get(Collection c) {

        switch (c) {
            case null -> System.out.println("Did you call the get with a null?");
            default -> System.out.println("default");
        }
    }

    public static void main(String[] argv) {
        get(null);
    }
}

At runtime, the application outputs:

Did you call the get with a null?

The case null can be combined with the default case as follows:

import java.util.Collection;

public class SampleClass {
    static void get(Collection c) {
        switch (c) {
            case null, default -> System.out.println("Did you call the get with a null?");
        }
    }

    public static void main(String[] argv) {
        get(null);
    }
}

However, the case null cannot be combined with any other case label. For example, the following class combines the case null with a case label with a pattern Stack s:

import java.util.Collection;
import java.util.Stack;

public class SampleClass {
    static void get(Collection c) {
        switch (c) {
            case null, Stack s -> System.out.println("Did you call the get with a null?");
            default -> System.out.println("default");
        }
    }

    public static void main(String[] args) {
        get(null);
    }
}

The class generates a compile-time error:

SampleClass.java:11: error: invalid case label combination
          case null, Stack s -> System.out.println("Did you call the get with a null?");

Guarded patterns with the when clause  

Sometimes, developers may use a conditional case label pattern that is matched based on the outcome of a boolean expression. This is where the when clause comes in handy. This clause evaluates a boolean expression, forming what is known as a ‘guarded pattern.’ For example, the when clause in the first case label in the following code snippet determines if a Stack is empty.

import java.util.Stack;
import java.util.Collection;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            case Stack s when s.empty() -> s.push("first");
            case Stack s2 -> s2.push("second");
            default -> c;
        };
    }
}

The corresponding code, located to the right of the ‘->‘, only executes if the Stack is indeed empty.

The ordering of the case labels with patterns is significant

When using case labels with patterns, developers must ensure an order that doesn’t create any issues related to type or subtype hierarchy. That is because, unlike constant case labels, patterns in case labels allow a selector expression to be compatible with multiple case labels containing patterns. The switch pattern matching feature matches the first case label, where the pattern matches the value of the selector expression.  

If the type of a case label pattern is a subtype of the type of another case label pattern that appears before it, a compile-time error will occur because the latter case label will be identified as unreachable code.  

To demonstrate this scenario, developers can compile and run the following sample class in which a case label pattern of type Object dominates a subsequent code label pattern of type Stack.

import java.util.Stack;

public class SampleClass {
    static Object get(Object c) {
        return switch (c) {
            case Object o  -> c;
            case Stack s  -> s.pop();
        };
    }
}

When compiling the class, an error message is produced:

SampleClass.java:12: error: this case label is dominated by a preceding case lab
el
        case Stack s  -> s.pop();
             ^

The compile-time error can be fixed simply by reversing the order of the two case labels as follows:

public class SampleClass {
    static Object get(Object c) {
        return switch (c) {
            case Stack s  -> s.pop();
            case Object o  -> c;
        };
    }
}

Similarly, if a case label includes a pattern that is of the same reference type as a preceding case label with an unconditional/unguarded pattern (guarded patterns discussed in an earlier section), it will result in a  compile-type error for the same reason, as in the class:

import java.util.Stack;
import java.util.Collection;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            case Stack s -> s.push("first");
            case Stack s2 -> s2.push("second");
        };
    }
}

Upon compilation, the following error message is generated:

SampleClass.java:13: error: this case label is dominated by a preceding case lab
el
        case Stack s2 -> s2.push("second");
             ^

To avoid such errors, developers should maintain a straightforward and readable ordering of case labels. The constant labels should be listed first, followed by the case null label, the guarded pattern labels, and the non-guarded type pattern labels. The default case label can be combined with the case null label, or placed separately as the last case label. The following class demonstrates the correct ordering:

import java.util.Collection;
import java.util.Stack;
import java.util.Vector;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            case null -> c;  //case label null
            case Stack s when s.empty() -> s.push("first");  // case label with guarded pattern
            case Vector v when v.size() > 2 -> v.lastElement();  // case label with guarded pattern
            case Stack s -> s.push("first");  // case label with unguarded pattern
            case Vector v -> v.firstElement();  // case label with unguarded pattern
            default -> c;
        };
    }
}

Pattern matching can be used with the traditional switch statement and with fall-through semantics

The pattern-matching feature is independent of whether it is a switch statement or a switch expression. The pattern matching is also independent of whether the fall-through semantics with case …: labels, or the no-fall-through semantics with the case …-> labels is used. In the following example, pattern matching is used with a switch statement and not a switch expression. The case labels use the fall-through semantics with the case …: labels. A when clause in the first case label uses a guarded pattern.  

import java.util.Stack;
import java.util.Collection;

public class SampleClass {
    static void get(Collection c) {
        switch (c) {
            case Stack s when s.empty(): s.push("first"); break;
            case Stack s : s.push("second");  break;
            default : break;
        }
    }
}

Scope of pattern variables

A pattern variable is a variable that appears in a case label pattern. The scope of a pattern variable is limited to the block, expression, or throw statement that appears to the right of the -> arrow. To demonstrate, consider the following code snippet in which a pattern variable from a preceding case label is used in the default case label.

import java.util.Stack;

public class SampleClass {
    static Object get(Object c) {
        return switch (c) {
            case Stack s -> s.push("first");
            default -> s.push("first");
        };
    }
}

A compile-time error results:

import java.util.Collection;
SampleClass.java:13: error: cannot find symbol
        default -> s.push("first");
                   ^
  symbol:   variable s
  location: class SampleClass

The scope of a pattern variable that appears in the pattern of a guarded case label includes the when clause, as demonstrated in the example:

import java.util.Stack;
import java.util.Collection;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            case Stack s when s.empty() -> s.push("first");
            case Stack s -> s.push("second");
            default -> c;
        };
    }
}

Given the limited scope of a pattern variable, the same pattern variable name can be used across multiple case labels. This is illustrated in the preceding example, where the pattern variable s is used in two different case labels.

When dealing with a case label with fall-through semantics, the scope of a pattern variable extends to the group of statements located to the right of the ‘:‘. That is why it was possible to use the same pattern variable name for the two case labels in the previous section by using pattern matching with the traditional switch statement. However, fall through case label that declares a pattern variable is a compile-time error. This can be demonstrated in the following variation of the earlier class:

import java.util.Stack;
import java.util.Vector;
import java.util.Collection;

public class SampleClass {
    static void get(Collection c) {
        switch (c) {
            case Stack s : s.push("second");
            case Vector v  : v.lastElement();
            default : System.out.println("default");
        }
    }
}

Without a break; statement in the first statement group, the switch could fall-through the second statement group without initializing the pattern variable v in the second statement group. The preceding class would generate a compile-time error:

SampleClass.java:12: error: illegal fall-through to a pattern
        case Vector v  : v.lastElement();
             ^

Simply adding a break; statement in the first statement group as follows fixes the error:

import java.util.Stack;
import java.util.Vector;
import java.util.Collection;

public class SampleClass {
    static void get(Collection c) {
        switch (c) {
            case Stack s : s.push("second"); break;
            case Vector v  : v.lastElement();
            default : System.out.println("default");
        }
    }
}

Only one pattern per case label

Combining multiple patterns within a single case label, whether it is a case label of the type case …:, or the type case …->  is not allowed, and it is a compile-time error. It may not be obvious, but combining patterns in a single case label incurs fall-through a pattern, as demonstrated by the following class.

import java.util.Stack;
import java.util.Vector;
import java.util.Collection;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            case Stack s, Vector v -> c;
            default -> c;
        };
    }
}

A compile-time error is generated:

SampleClass.java:11: error: illegal fall-through from a pattern
        case Stack s, Vector v -> c;
                      ^

Only one match-all case label in a switch block

It is a compile-time error to have more than one match-all case labels in a switch block, whether it is a switch statement or a switch expression. The match-all case labels are :

  1. A case label with a pattern that unconditionally matches the selector expression
  2. The default case label

To demonstrate, consider the following class:

import java.util.Collection;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            case Collection coll -> c;
            default -> c;
        };
    }
}

Compile the class, only to get an error message:

SampleClass.java:13: error: switch has both an unconditional pattern and a default label
        default -> c;
        ^

The exhaustiveness of type coverage

Exhaustiveness implies that a switch block must handle all possible values of the selector expression. The exhaustiveness requirement is implemented only if one or more of the following apply:

  • a) Pattern switch expressions/statements are used,
  • b) The case null is used,
  • c) The selector expression is not one of the legacy types (char, byte, short, int, Character, Byte, Short, Integer, String, or an enum type).

To implement exhaustiveness, it may suffice to add case labels for each of the subtypes of the selector expression type if the subtypes are few.  However, this approach could be tedious if subtypes are numerous; for example, adding a case label for each reference type for a selector expression of type Object, or even each of the subtypes for a selector expression of type Collection, is just not feasible.

To demonstrate the exhaustiveness requirement, consider the following class:

import java.util.Collection;
import java.util.Stack;
import java.util.LinkedList;
import java.util.Vector;

public class SampleClass {
    static Object get(Collection c)   {
        return switch (c) {
            case Stack s  -> s.push("first");
            case null  -> throw new NullPointerException("null");
            case LinkedList l    -> l.getFirst();
            case Vector v  -> v.lastElement();
        };
    }
}  

The class generates a compile-time error message:

SampleClass.java:10: error: the switch expression does not cover all possible in
put values
                return switch (c) {
                       ^

The issue can be fixed simply by adding a default case as follows:

import java.util.Collection;
import java.util.Stack;
import java.util.LinkedList;
import java.util.Vector;

public class SampleClass {
    static Object get(Collection c)   {
        return switch (c) {
            case Stack s  -> s.push("first");
            case null  -> throw new NullPointerException("null");
            case LinkedList l    -> l.getFirst();
            case Vector v  -> v.lastElement();
            default -> c;
        };
    }
}  

A match-all case label with a pattern that unconditionally matches the selector expression, such as the one in the following class, would be exhaustive, but it wouldn’t handle or process any subtypes distinctly.

import java.util.Collection;

public class SampleClass {
    static Object get(Collection c)   {
        return switch (c) {
            case Collection coll  -> c;
        };
    }
}  

The default case label could be needed for exhaustiveness but could sometimes be avoided if the possible values of a selector expression are very few. As an example, if the selector expression is of type java.util.Vector, only one case label pattern for the single subclass java.util.Stack is required to avoid the default case. Similarly, if the selector expression is a sealed class type, only the classes declared in the permits clause of the sealed class type need to be handled by the switch block.

Generics record patterns in switch case label

Java 20 adds support for an inference of type arguments for generic record patterns in switch statements/expressions. As an example, consider the generic record:

record Triangle(S firstCoordinate, T secondCoordinate,V thirdCoordinate){};

In the following switch block, the inferred record pattern is

Triangle(var f, var s, var t):
 
static void getPt(Triangle tr){
        switch (tr) {
           case Triangle(var f, var s, var t) -> …;
           case default -> …;
        }
}

Error handling with MatchException

Java 19 introduces a new subclass of the java.lang.Runtime class for a more uniform exception handling during pattern matching. The new class called java.lang.MatchException is a preview API. The MatchException is not designed specifically for pattern matching in a switch but rather for any pattern-matching language construct. MatchException may be thrown at runtime when an exhaustive pattern matching does not match any of the provided patterns. To demonstrate this, consider the following application that includes a record pattern in a case label for a record that declares an accessor method with division by 0.

record DivisionByZero(int i) {
    public int i() {
        return i / 0;
    }
}


public class SampleClass {

    static DivisionByZero get(DivisionByZero r) {
        return switch(r) {
        case DivisionByZero(var i) -> r;
        };

    }

    public static void main(String[] argv) {

        get(new DivisionByZero(42));
    }
}

The sample application compiles without an error but at runtime throws a MatchException:

Exception in thread "main" java.lang.MatchException: java.lang.ArithmeticException: / by zero
        at SampleClass.get(SampleClass.java:7)
        at SampleClass.main(SampleClass.java:14)
Caused by: java.lang.ArithmeticException: / by zero
        at DivisionByZero.i(SampleClass.java:1)
        at SampleClass.get(SampleClass.java:1)
        ... 1 more

Conclusion

This article introduces the new pattern-matching support for the switch control-flow construct. The main improvements are that switch’s selector expression can be any reference type, and the switch’s case labels can include patterns, including conditional pattern matching. And, if you rather not update your complete codebase, pattern matching is supported with traditional switch statements and with traditional fall-through semantics.