2024 DevOps Lifecycle: Share your expertise on CI/CD, deployment metrics, tech debt, and more for our Feb. Trend Report (+ enter a raffle!).
Kubernetes in the Enterprise: Join our Virtual Roundtable as we dive into Kubernetes over the past year, core usages, and emerging trends.
Software design and architecture focus on the development decisions made to improve a system's overall structure and behavior in order to achieve essential qualities such as modifiability, availability, and security. The Zones in this category are available to help developers stay up to date on the latest software design and architecture trends and techniques.
Cloud architecture refers to how technologies and components are built in a cloud environment. A cloud environment comprises a network of servers that are located in various places globally, and each serves a specific purpose. With the growth of cloud computing and cloud-native development, modern development practices are constantly changing to adapt to this rapid evolution. This Zone offers the latest information on cloud architecture, covering topics such as builds and deployments to cloud-native environments, Kubernetes practices, cloud databases, hybrid and multi-cloud environments, cloud computing, and more!
Containers allow applications to run quicker across many different development environments, and a single container encapsulates everything needed to run an application. Container technologies have exploded in popularity in recent years, leading to diverse use cases as well as new and unexpected challenges. This Zone offers insights into how teams can solve these challenges through its coverage of container performance, Kubernetes, testing, container orchestration, microservices usage to build and deploy containers, and more.
Integration refers to the process of combining software parts (or subsystems) into one system. An integration framework is a lightweight utility that provides libraries and standardized methods to coordinate messaging among different technologies. As software connects the world in increasingly more complex ways, integration makes it all possible facilitating app-to-app communication. Learn more about this necessity for modern software development by keeping a pulse on the industry topics such as integrated development environments, API best practices, service-oriented architecture, enterprise service buses, communication architectures, integration testing, and more.
A microservices architecture is a development method for designing applications as modular services that seamlessly adapt to a highly scalable and dynamic environment. Microservices help solve complex issues such as speed and scalability, while also supporting continuous testing and delivery. This Zone will take you through breaking down the monolith step by step and designing a microservices architecture from scratch. Stay up to date on the industry's changes with topics such as container deployment, architectural design patterns, event-driven architecture, service meshes, and more.
Performance refers to how well an application conducts itself compared to an expected level of service. Today's environments are increasingly complex and typically involve loosely coupled architectures, making it difficult to pinpoint bottlenecks in your system. Whatever your performance troubles, this Zone has you covered with everything from root cause analysis, application monitoring, and log management to anomaly detection, observability, and performance testing.
The topic of security covers many different facets within the SDLC. From focusing on secure application design to designing systems to protect computers, data, and networks against potential attacks, it is clear that security should be top of mind for all developers. This Zone provides the latest information on application vulnerabilities, how to incorporate security earlier in your SDLC practices, data governance, and more.
Observability and Application Performance
Making data-driven decisions, as well as business-critical and technical considerations, first comes down to the accuracy, depth, and usability of the data itself. To build the most performant and resilient applications, teams must stretch beyond monitoring into the world of data, telemetry, and observability. And as a result, you'll gain a far deeper understanding of system performance, enabling you to tackle key challenges that arise from the distributed, modular, and complex nature of modern technical environments.Today, and moving into the future, it's no longer about monitoring logs, metrics, and traces alone — instead, it’s more deeply rooted in a performance-centric team culture, end-to-end monitoring and observability, and the thoughtful usage of data analytics.In DZone's 2023 Observability and Application Performance Trend Report, we delve into emerging trends, covering everything from site reliability and app performance monitoring to observability maturity and AIOps, in our original research. Readers will also find insights from members of the DZone Community, who cover a selection of hand-picked topics, including the benefits and challenges of managing modern application performance, distributed cloud architecture considerations and design patterns for resiliency, observability vs. monitoring and how to practice both effectively, SRE team scalability, and more.
Building Robust Real-Time Data Pipelines With Python, Apache Kafka, and the Cloud
When building a large production-ready stateless microservices architecture, we always come across a common challenge of preserving request context across services and threads, including context propagation to the child threads. What Is Context Propagation? Context propagation means passing contextual information or states across different components or services in a distributed system where applications are often composed of multiple services running on different machines or containers. These services need to communicate and collaborate to fulfill a user request or perform a business process. Context propagation becomes crucial in such distributed systems to ensure that relevant information about a particular transaction or operation is carried along as it traverses different services. This context may include data such as: User authentication details Request identifiers Distributed Tracing information Other metadata (that helps in understanding the state and origin of a request) Key aspects of context propagation include: Request Context: When a user initiates a request, it often triggers a chain of interactions across multiple services. The context of the initial request, including relevant information like user identity, request timestamp, and unique identifiers, needs to be propagated to ensure consistent behavior and tracking. Distributed Tracing and Logging: Context propagation is closely tied to distributed tracing and logging mechanisms. By propagating context information, it becomes easier to trace the flow of a request through various services, aiding in debugging, performance analysis, and monitoring. Consistency: Maintaining a consistent context across services is essential for ensuring that each service involved in handling a request has the necessary information to perform its tasks correctly. This helps avoid inconsistencies and ensures coherent behavior across the distributed system. Middleware and Framework Support: Many middleware and frameworks provide built-in support for context propagation. For example, in microservices architectures, frameworks like Spring Cloud, Istio, or Zipkin offer tools for managing and propagating context seamlessly. Statelessness: Context propagation is especially important in stateless architectures where each service should operate independently without relying on a shared state. The context helps in providing the necessary information for a service to process a request without needing to store a persistent state. Effective context propagation contributes to the overall reliability, observability, and maintainability of distributed systems by providing a unified view of the state of a transaction as it moves through different services. It also helps in reducing the code. The Usecase Let's say you are building a Springboot Webflux-based Microservices/applications, and you need to ensure that the state of the user (Session Identifier, Request Identifier, LoggedIn Status, etc. ) and client ( Device Type, Client IP, etc.) passed in the originating request should be passed between the services. The Challenges Service-to-service call: For internal service-to-service calls, the context propagation does not happen automatically. Propagating context within classes: To refer to the context within service and/or helper classes, you need to explicitly pass it via the method arguments. This can be handled by creating a class with a static method that stores the context in the ThreadLocal object. Java Stream Operations: Since Java stream functions run in separate executor threads, the Context propagation via ThreadLocal to child threads needs to be done explicitly. Webflux: Similar to Java Stream functions, Context propagation in Webflux needs to be handled via reactor Hooks. The Idea here is how to ensure that context propagation happens automatically in the child threads and to the internal called service using a reactive web client. A similar pattern can be implemented for Non reactive code also. Solution Core Java provides two classes, ThreadLocal and InheritableThreadLocal, to store thread-scoped values. ThreadLocal allows the creation of variables that are local to a thread, ensuring each thread has its own copy of the variable. A limitation of ThreadLocal is that if a new thread is spawned within the scope of another thread, the child thread does not inherit the values of ThreadLocal variables from its parent. Java public class ExampleThreadLocal { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { threadLocal.set("Main Thread Value"); new Thread(() -> { System.out.println("Child Thread: " + threadLocal.get()); // Outputs: Child Thread: null }).start(); System.out.println("Main Thread: " + threadLocal.get()); // Outputs: Main Thread: Main Thread Value } } On the other hand; InheritableThreadLocal extends ThreadLocal and provides the ability for child threads to inherit values from their parent threads. Java public class ExampleInheritableThreadLocal { private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { inheritableThreadLocal.set("Main Thread Value"); new Thread(() -> { System.out.println("Child Thread: " + inheritableThreadLocal.get()); // Outputs: Child Thread: Main Thread Value }).start(); System.out.println("Main Thread: " + inheritableThreadLocal.get()); // Outputs: Main Thread: Main Thread Value } } Hence, in the scenarios where we need to ensure that context must be propagated between parent and child threads, we can use application-scoped static InheritableThreadLocal variables to hold the context and fetch it wherever needed. Java @Getter @ToString @Builder public class RequestContext { private String sessionId; private String correlationId; private String userStatus; private String channel; } Java public class ContextAdapter { final ThreadLocal<RequestContext> threadLocal = new InheritableThreadLocal<>(); public RequestContext getCurrentContext() { return threadLocal.get(); } public void setContext(tRequestContext requestContext) { threadLocal.set(requestContext); } public void clear() { threadLocal.remove(); } } Java public final class Context { static ContextAdapter contextAdapter; private Context() {} static { contextAdapter = new ContextAdapter(); } public static void clear() { if (contextAdapter == null) { throw new IllegalStateException(); } contextAdapter.clear(); } public static RequestContext getContext() { if (contextAdapter == null) { throw new IllegalStateException(); } return contextAdapter.getCurrentContext(); } public static void setContext(RequestContext requestContext) { if (cContextAdapter == null) { throw new IllegalStateException(); } contextAdapter.setContext(requestContext); } public static ContextAdapter getContextAdapter() { return contextAdapter; } } We can then refer to the context by calling the static method wherever required in the code. Java Context.getContext() This solves for: Propagating context within classes. Java Stream Operations Webflux In order to ensure that context is propagated to external calls via webclient, automatically, we can create a custom ExchangeFilterFunctionto read the context from Context.getContext() and then add the context to the header or query params as required. Java public class HeaderExchange implements ExchangeFilterFunction { @Override public Mono<ClientResponse> filter( ClientRequest clientRequest, ExchangeFunction exchangeFunction) { return Mono.deferContextual(Mono::just) .flatMap( context -> { RequestContext currentContext = Context.getContext(); ClientRequest newRequest = ClientRequest.from(clientRequest) .headers(httpHeaders ->{ httpHeaders.add("context-session-id",currentContext.getSessionId() ); httpHeaders.add("context-correlation-id",currentContext.getCorrelationId() ); }).build(); return exchangeFunction.exchange(newRequest); }); } } Initializing the Context as part of WebFilter. Java @Slf4j @Component public class RequestContextFilter implements WebFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { String sessionId = exchange.getRequest().getHeaders().getFirst("context-session-id"); String correlationId = exchange.getRequest().getHeaders().getFirst("context-correlation-id"); RequestContext requestContext = RequestContext.builder().sessionId(sessionId).correlationId(correlationId).build() Context.setContext(requestContext); return chain.filter(exchange); } }
In the previous articles, you learned about the virtual threads in Java 21 in terms of history, benefits, and pitfalls. In addition, you probably got inspired by how Quarkus can help you avoid the pitfalls but also understood how Quarkus has been integrating the virtual threads to Java libraries as many as possible continuously. In this article, you will learn how the virtual thread performs to handle concurrent applications in terms of response time, throughput, and resident state size (RSS) against traditional blocking services and reactive programming. Most developers including you and the IT Ops teams also wonder if the virtual thread could be worth replacing with existing business applications in production for high concurrency workloads. Performance Applications I’ve conducted the benchmark testing with the Todo application using Quarkus to implement 3 types of services such as imperative (blocking), reactive (non-blocking), and virtual thread. The Todo application implements the CRUD functionality with a relational database (e.g., PostgreSQL) by exposing REST APIs. Take a look at the following code snippets for each service and how Quarkus enables developers to implement the getAll() method to retrieve all data from the Todo entity (table) from the database. Find the solution code in this repository. Imperative (Blocking) Application In Quarkus applications, you can make methods and classes with @Blocking annotation or non-stream return type (e.g. String, List). Java @GET public List<Todo> getAll() { return Todo.listAll(Sort.by("order")); } Virtual Threads Application It’s quite simple to make a blocking application into a virtual thread application. As you see in the following code snippets, you just need to add a @RunOnVirtualThread annotation into the blocking service, getAll() method. Java @GET @RunOnVirtualThread public List<Todo> getAll() { return Todo.listAll(Sort.by("order")); } Reactive (Non-Blocking) Application Writing a reactive application should be a big challenge for Java developers when they need to understand the reactive programming model and the continuation and event stream handler implementation. Quarkus allows developers to implement both non-reactive and reactive applications in the same class because Quarkus is built on reactive engines such as Netty and Vert.x. To make an asynchronous reactive application in Quarkus, you can add a @NonBlocking annotation or set the return type with Uni or Multi in the SmallRye Mutiny project as below the getAll() method. Java @GET public Uni<List<Todo>> getAll() { return Panache.withTransaction(() -> Todo.findAll(Sort.by("order")).list()); } Benchmark scenario To make the test result more efficient and fair, we’ve followed the Techempower guidelines such as conducting multiple scenarios, running on bare metal, and containers on Kubernetes. Here is the same test scenario for the 3 applications (blocking, reactive, and virtual threads), as shown in Figure 1. Fetch all rows from a DB (quotes) Add one quote to the returned list Sort the list Return the list as JSON Figure 1: Performance test architecture Response Time and Throughput During the performance test, we’ve increased the concurrency level from 1200 to 4400 requests per second. As you expected, the virtual thread scaled better than worker threads (traditional blocking services) in terms of response time and throughput. More importantly, it didn’t outperform the reactive service all the time. When the concurrent level reached 3500 requests per second, the virtual threads went way slower and lower than the worker threads. Figure 2: Response time and throughput Resource Usage (CPU and RSS) When you design a concurrent application regardless of cloud deployment, you or your IT Ops team need to estimate the resource utilization and capacity along with high scalability. The CPU and RSS (resident set size) usage is a key metric to measure resource utilization. With that, when the concurrency level reached out to 2000 requests per second in CPU and Memory usage, the virtual threads turned rapidly higher than the worker threads. Figure 3: Resource usage (CPU and RSS) Memory Usage: Container Container runtimes (e.g., Kubernetes) are inevitable to run concurrent applications with high scalability, resiliency, and elasticity on the cloud. The virtual threads had lower memory usage inside the limited container environment than the worker thread. Figure 4: Memory usage - Container Conclusion You learned how the virtual threads performed in multiple environments in terms of response time, throughput, resource usage, and container runtimes. The virtual threads seem to be better than the blocking services on the worker threads all the time. But when you look at the performance metrics carefully, the measured performance went down than the blocking services at some concurrent levels. On the other hand, the reactive services on the event loops were always higher performed than both the virtual and worker threads all the time. Thus, the virtual thread can provide high enough performance and resource efficiency based on your concurrency goal. Of course, the virtual thread is still quite simple to develop concurrent applications without a steep learning curve as the reactive programming.
With the rapid adoption of passkeys (and the underlying WebAuthn protocol), authentication has become more secure and user-friendly for many users. One of the standout advancements of passkeys has been the integration of Conditional UI, often referred to as "passkey autofill" or Conditional Mediation (in the following, we stay with the term Conditional UI). Despite its recent introduction and ongoing adoption by browsers, there’s a noticeable gap in technical documentation and implementation advice for Conditional UI. This article aims to bridge that gap by explaining what Conditional UI is, how it works, and how to tackle common challenges during its implementation. What Is Conditional UI? Conditional UI represents a new mode for passkeys/WebAuthn login processes. It selectively displays passkeys in a user interface (UI) only when a user has a discoverable credential (resident key), which is a type of passkey registered with the relying party (the online service) stored in their authenticator of a device (e.g., laptop, smartphone). The passkeys are displayed in a selection dropdown that is mixed up with auto-filled passwords, providing a seamless transition between traditional password systems and advanced passkey authentication, as users see both in the same context. This intelligent approach ensures that users aren't overwhelmed with unnecessary options and can navigate the login process more seamlessly. The foundation of Conditional UI is built on three main pillars: Respect user privacy: Ensuring user privacy by preventing disclosure of available credentials or lack of user consent to reveal these credentials. Great user experience even if no passkey exists: Empowering relying parties to implement WebAuthn opportunistically, ensuring user experience remains good even if passkeys are not available. Smooth transition from passwords to passkeys: Combining passkeys with password-based authentication to smooth the transition towards passwordless authentication methods, capitalizing on users' familiar UX paradigms. Conditional UI Benefits and Drawbacks Benefits Streamlined authentication: The process is more streamlined and efficient, removing the complexities often associated with multiple authentication methods. Reduction in user errors: By presenting only relevant options, users are less likely to make mistakes during the authentication process. Enhanced user satisfaction: Removing unnecessary steps means users can log in faster and more effortlessly, leading to improved user satisfaction. Simple frontend integration: One of the standout features of Conditional UI is its ease of integration. Developers can seamlessly incorporate it into the front end with a few lines of code (see below). Passwordless and usernameless login: One of the huge benefits is that Conditional UI promotes not only passwordless authentication but also a usernameless or accountless experience. Users are spared the mental load of recalling their specific email address or user handle from sign-up. Instead, they can rely on the browser’s suggestions, which include the email address/user handle paired with the appropriate passkey in the autofill menu. Solving the bootstrapping dilemma: Transitioning from traditional username-password systems to passkeys can be daunting. Conditional UI addresses this transition challenge. Websites can initiate a passkey / WebAuthn call alongside a conventional password prompt without fretting over potential modal dialog errors if a device lacks the needed credentials. Drawbacks Learning curve for developers: Conditional UI introduces a new paradigm, which means there's a learning curve involved for developers unfamiliar with its intricacies. Device/browser dependency: Conditional UI’s success hinges on the user's device or browser compatibility. Given that not all browsers or devices support it currently, this can limit its application. No conditional passkey register: There’s no support for using Conditional UI in the account/passkey creation process. That means you need to create passkeys the regular way, either at the account creation stage by providing some dedicated passkey creation page or in the account settings. However, there's an ongoing discourse about the potential inclusion of Conditional UI for sign-ups as well. Password manager disable autocomplete: Some modern password managers and their browser extensions modify the website’s DOM and disable or overwrite the autocomplete tag in input fields in favor of their own autocomplete features. This can lead to an inconsistent and unsatisfying user experience. As standards for Conditional UI are relatively new, we hope that things improve so that, e.g., not two autofill menus are overlaid or the desired one is not shown at all. How Does Conditional UI Work? In the following, we provide a step-by-step breakdown of the single steps of an entire Conditional UI flow: In general, the Conditional UI process flow can be partitioned into two phases. During the page load phase, conditional UI logic happens in the background, while in the user operation phase, the user has to do something actively. Conditional UI availability checks: The client (browser) calls the isConditionalMediationAvailable() function to detect if the current browser/device combination supports Conditional UI.Only if the response is true does the process continue; otherwise, the Conditional UI process is aborted. Call the conditional UI endpoint: Next, the client calls the server Conditional UI endpoint in order to retrieve the PublicKeyCredentialRequestOptions. Receive PublicKeyCredentialRequestOptions: The server returns the PublicKeyCredentialRequestOptions which contain the challenge and more WebAuthn server options (e.g., allowCredentials, extensions, userVerification). Start the local authentication: By calling credentials.get() with the received PublicKeyCredentialOptions and the mediation property is set to be “conditional, the process for the local authentication on the device starts. Show autofill selection: The autofill menu for passkeys pops up. The specific styling is dependent on the browser and device (e.g., some require the user to place the cursor in the input field, and some automatically display the menu on page load; see below). Local user authentication: The user selects the passkey from the autofill menu that they want to use and authenticate via the authentication dialog of their device (e.g., via Face ID, Touch ID, Windows Hello). Send authenticator response to server: If the local user authentication was successful, the authenticator response is sent back to the server. User is logged in and redirected: Once the server receives the authenticator response, it validates the signature against the corresponding user account’s public key in the database. If the verification is successful, the user is granted access, logged in, and redirected to the logged-in page. By following this process flow, Conditional UI offers a seamless and user-friendly authentication experience. Technical Requirements for Conditional UI General To get Conditional UI working, some general aspects need to be considered: Credential specifications: Conditional UI is specifically designed to operate only with resident keys/discoverable credentials. The reason behind this is that authenticators do not store user-specific data (e.g., name, display name) for non-resident keys/non-discoverable credentials. As a result, using the latter for passkey autofill is not possible. Credential filtering: The allowCredentials feature remains supported, facilitating websites that are already aware of the user's identity (for instance, if a username was sent in the initial mediation call because it might be stored in the browser’s LocalStorage) to refine the list of credentials showcased to users during autofill. Client-Side To get Conditional UI working on the client side, the following requirements must be fulfilled: Compatible browser: Ensure that the user uses a modern browser that supports Conditional UI (see here for the latest browser coverage). Enabled JavaScript: JavaScript must be enabled to facilitate Conditional UI operations. Test conditional UI availability: The relying party (the server side) should have the certainty that Conditional UI is available on the client side when it receives the WebAuthn mediation call to avoid triggering any user-visible errors in scenarios where Conditional UI isn't supported. To address this, it’s recommended to use the isConditionalMediationAvailable() method and check for the technical availability of Conditional UI. HTML input field required: For Conditional UI to work, you need to have an HTML input field on your web page. If you do not have one, you need to provide support for the regular passkey/WebAuthn login process that is triggered with a user interaction, like a button click. Remove timeout protocols: Timeout parameters (e.g., the user is taking a very long time to decide on a passkey in the autofill menu) should be disregarded in this setup. Server-Side To get Conditional UI working, some requirements on the server side must be fulfilled as well: Running WebAuthn server: As we are still in the context of passkeys / WebAuthn, it’s required to have a WebAuthn server running that manages the authentication procedures. Provide mediation start endpoint: Compared to regular WebAuthn login endpoints, it’s useful to provide another endpoint that has similar functionality but can deal with an optional user handle (e.g., email address, phone number, username). Practical Coding Tips Since the official rollout of Conditional UI in late 2022 and earlier beta versions, we’ve been testing and working extensively with it. In the following, we want to share practical tips that helped during the implementation of Conditional UI with you. Full Conditional UI Example A full, minimalistic code example for a Conditional UI method would look like this: HTML <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Conditional UI</title> </head> <body> <input type="text" id="username" autoComplete="username webauthn" /> <button onclick="passkeyLogin()">Login via passkey</button> </body> </html> 6.2 Browser Compatibility Check Implement Conditional UI detection that ensures that Conditional UI is only employed when the current device/browser combination supports it. This should work without presenting user-visible errors in the absence of Conditional UI support. Incorporating the isConditionalMediationAvailable() method within the user interface addresses this concern. If Conditional UI support is given, the Conditional UI login process can be started. JavaScript // source: https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/isConditionalMediationAvailable#examples // Availability of `window.PublicKeyCredential` means WebAuthn is usable. if ( window.PublicKeyCredential && PublicKeyCredential.isConditionalMediationAvailable ) { // Check if conditional mediation is available. const isCMA = await PublicKeyCredential.isConditionalMediationAvailable(); if (isCMA) { // Call WebAuthn authentication start endpoint let options = await WebAuthnClient.getPublicKeyRequestOptions(); const credential = await navigator.credentials.get({ publicKey: options.publicKeyCredentialRequestOptions, mediation: "conditional", }); /* ... */ } } WebAuthn Autocomplete Token in Input Fields The input field should receive a “webauthn” HTML autofill token. This signals the client to populate passkeys to the ongoing request. Besides passkeys, other autofill values might be showcased as well. These autofill tokens can be paired with other existing tokens, e.g.: autocomplete="username webauthn": Besides displaying passkeys, this also suggests username autofill. autocomplete="current-password webauthn": Besides displaying passkeys, this further prompts for password autofill. HTML <label for="name">Username:</label> <input type="text" name="name" autocomplete="username webauthn"> <label for="password">Password:</label> <input type="password" name="password" autocomplete="current-password webauthn"> Mediation Property in WebAuthn API Get Call To retrieve available passkeys after having received the PublicKeyCredentialRequestOptions object, the navigator.credentials.get() function should be called (which serves both passkeys and passwords). The PublicKeyCredentialRequestOptions object needs to have the mediation parameter set to “conditional” to activate Conditional UI on the client. JavaScript const credential = await navigator.credentials.get({ publicKey: options.publicKeyCredentialRequestOptions, mediation: "conditional" }); Cancellation of Conditional UI Flow If there's no available passkey, or the user neglects the suggested passkeys and enters their email, the Conditional UI flow is stopped. This underscores the importance of always supporting the standard passkey / WebAuthn login via a modal as well. A critical point to emphasize here is the potential need to halt an ongoing Conditional UI request. Contrary to modal experiences, autofill dropdowns lack a cancellation button. As per WebAuthn's design, only a single active credential request should be in progress at any given moment. The WebAuthn standard suggests utilizing an AbortController to cancel a WebAuthn process, applicable to both regular and Conditional UI login processes (see WebAuthn's Docs for details). The Conditional UI login process gets activated as soon as a user lands on the page. The initial task should be to create a globally-scoped AbortController object. This will act as a signal for your client to terminate the autofill request, especially if the user decides to do the regular passkey login process. Reassure that the AbortController can be invoked by other functions and is reset if the Conditional UI process has to restart. Employ the signal property within the navigator.credentials.get() call, incorporating your AbortController signal as its value. This signals to the passkey / WebAuthn function that the request must be halted if the signal gets aborted. Remember to set up a fresh AbortController each time you trigger Conditional UI. Using an already-aborted AbortController will lead to an instant cancellation of the passkey / WebAuthn function. The remaining steps align with a regular passkey login process. In the following, you see a code example of the mentioned steps: HTML <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Conditional UI</title> </head> <body> <input type="text" id="username" autoComplete="username webauthn" /> <button onclick="passkeyLogin()">Login via passkey</button> </body> </html> In the absence of Conditional UI support, direct users towards the regular passkey login process. Offering this path is important for users relying on hardware security keys (e.g., YubiKeys) or those compelled to use non-resident keys / non-discoverable credentials due to authenticator constraints. Conditional UI in Native Apps When you develop a native app, e.g., for iOS or Android, Conditional UI works as well. It doesn’t matter if you implement it natively in Flutter, Kotlin, or Swift or if you decide to go with Chrome Custom Tabs CCT or SFSafariViewController or SFAuthenticationSession / ASWebAuthenticationSession. Both approaches support Conditional UI. Conditional UI Examples in Different Devices/Browser To illustrate how Conditional UI looks like for the end user, we added several screenshots of a Conditional UI autofill menu using https://passkeys.eu. Conditional UI in Windows 11 (22H2) + Chrome 118 Conditional UI in macOS Ventura (13.5.1) + Chrome 118 Conditional UI in macOS Ventura (13.5.1) + Safari 16.6 Conditional UI in Android 13 + Chrome 118 Conditional UI in iOS 17.1 + Safari 17.1 Conclusion Passkeys, with its Conditional UI/passkey autofill capability, are the new way to authenticate online. As we transition to an era where passwords are more and more replaced by passkeys, the need for robust and user-friendly transition mechanisms is undeniable. We hope this article has helped you to understand how to correctly implement Conditional UI, handle the transition process, and which aspects to pay special attention to.
Amazon MemoryDB for Redis has supported username/password-based authentication using Access Control Lists since the very beginning. But you can also use IAM-based authentication that allows you to associate IAM users and roles with MemoryDB users so that applications can use IAM credentials to authenticate to the MemoryDB cluster. With this authentication method, you don't need to use a (long-lived) user password. Instead, you use an authentication token generated using AWS Signature Version 4. There are many benefits to this approach. Instead of managing username and password-based credentials, you can use IAM to centrally manage access to MemoryDB clusters. For client applications running on Amazon EC2, Amazon EKS, AWS Lambda, AWS App Runner, etc., you can inject these credentials (depending on the platform e.g. profile credentials in EC2 and instance role in App Runner) - this provides greater security. MemoryDB documentation has an example for a Java application with the Lettuce client. The process is similar for other languages, but you still need to implement it. So, let's learn how to do it for a Go application with the widely used go-redis client. As a bonus, this is also applicable to ElastiCache for Redis, which also supports IAM authentication. There are minor differences which I will list at the end of the blog. Let's get started... 1. Configure the MemoryDB Cluster Along With User and IAM Role Start by creating a MemoryDB user, Access Control list (ACL) and add the user to it. Make sure to use IAM as the authentication type. Now, create the MemoryDB cluster. Make sure to choose version 7.0 or above and choose the ACL you created before. While the cluster is being provisioned, create an IAM role. Use the following Trust Relationship to allow connectivity from an EC2 instance: { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "sts:AssumeRole" ], "Principal": { "Service": [ "ec2.amazonaws.com" ] } } ] } Use the following permissions: replace the MemoryDB cluster, IAM username, AWS account ID, and region. { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ memorydb:Connect" ], "Resource": [ "arn:aws:memorydb:<enter aws region>:<enter aws account ID>:cluster/<enter memorydb cluster name>", "arn:aws:memorydb:<enter aws region>:<enter aws account ID>:user/<enter memorydb iam user name>" ] } ] } 2. Configure AWS Cloud9 Environment Create an AWS Cloud9 environment to execute the client application that will connect to MemoryDB. Make sure to use the same VPC and subnet as the MemoryDB cluster Update the MemoryDB subnet group to add an inbound rule for the client application connectivity. Refer to the Getting Started with Redis on AWS - the easy way! blog post that already has this covered. 3. Run the Client App Once the MemoryDB cluster and Cloud9 instance are ready, go ahead and execute the client application. Open the Cloud9 instance terminal, and clone the following GitHub repository: git clone https://github.com/build-on-aws/aws-redis-iam-auth-golang cd aws-redis-iam-auth-golang Update the run.sh file to enter the MemoryDB cluster endpoint and username. For example: export SERVICE_NAME=memorydb export CLUSTER_NAME=demo-iam-cluster export CLUSTER_ENDPOINT=clustercfg.demo-iam-cluster.xyzxy4.memorydb.us-west-1.amazonaws.com:6379 export USERNAME=demo-iam-user Once the app is up and running, simply invoke its HTTP endpoints to verify that it works with IAM authentication. curl -i -X POST -d '{"key":"foo1", "value":"bar2"}' localhost:8080 curl -i -X POST -d '{"key":"foo2", "value":"bar2"}' localhost:8080 curl -i localhost:8080/foo1 curl -i localhost:8080/not_there The application is super simple. That's on purpose. Let's move on to the important bits, which is about the IAM authentication part. IAM Authentication Logic: Behind the Scenes You can refer to the authentication code in the GitHub repository. 1. We first create a HTTP GET request that contains the cluster name as part of the URL along with the username, the action (connect) and token expiry (900 seconds) as query parameters: //... queryParams := url.Values{ "Action": {connectAction}, "User": {userName}, "X-Amz-Expires": {strconv.FormatInt(int64(tokenValiditySeconds), 10)}, } authURL := url.URL{ Host: clusterName, Scheme: "http", Path: "/", RawQuery: queryParams.Encode(), } req, err := http.NewRequest(http.MethodGet, authURL.String(), nil) //... 2. The request is then signed using PresignHTTP: func (atg AuthTokenGenerator) Generate() (string, error) { signedURL, _, err := atg.signer.PresignHTTP( context.Background(), atg.credentials, atg.req, hexEncodedSHA256EmptyString, atg.serviceName, atg.region, time.Now().UTC(), ) //... } 3. The token generator is invoked in a CredentialsProvider during client creation: //... generator, err := auth.New(serviceName, clusterName, username, region) client = redis.NewClusterClient( &redis.ClusterOptions{ Username: username, Addrs: []string{clusterEndpoint}, NewClient: func(opt *redis.Options) *redis.Client { return redis.NewClient(&redis.Options{ Addr: opt.Addr, CredentialsProvider: func() (username string, password string) { token, err := generator.Generate() return opt.Username, token }, TLSConfig: &tls.Config{InsecureSkipVerify: true}, }) }, }) //... This Can Also Be Used for ElastiCache! The same approach applies - with a few changes. First, the IAM policy has to be updated to reflect ElastiCache resources (obviously!). For example: { "Version": "2012-10-17", "Statement": [ { "Effect" : "Allow", "Action" : [ "elasticache:connect" ], "Resource" : [ "arn:aws:elasticache:<enter aws region>:<enter aws account id>:<enter type - replicationgroup or serverlesscache>:<enter replicationgroup or serverlesscache name>", "arn:aws:elasticache:<enter aws region>:<enter aws account id>:user:<enter username>" ] } ] } Before you run the application, update the SERVICE_NAME environment variable to elasticache as well as the endpoint URL, cluster name, and IAM username for the ElastiCache instance. This example assumes you are using a Redis Cluster connection mode (which is the only option in the case of MemoryDB). But in the case of ElastiCache, be mindful of whether you are using cluster-mode-enabled configuration. If not, you will have to tweak the code to use redis.NewClient (instead of redis.NewClusterClient). The CredentialsProvider option will be available, nonetheless. Conclusion Using IAM authentication has its benefits. Instead of managing usernames and passwords in multiple locations/applications, delegate the heavy lifting to IAM. All you need is to provide configure appropriate permissions (principle of "least privilege"). But you also need to be aware of the limitations; e.g., IAM authentication is not supported in MULTI EXEC commands. For a complete list, refer to the documentation. Have you tried using IAM authentication with MemoryDB or ElastiCache for Redis in other programming languages? Let me know. Happy building!
Kubernetes' CronJob API is a pivotal feature for automating regular tasks in a cloud-native environment. This guide not only walks you through the steps to use this API but also illustrates practical use cases where it can be highly beneficial. Prerequisites A running Kubernetes Cluster (version 1.21 or later) kubectl Command Line Tool Basic Kubernetes knowledge (Pods, Jobs, CronJobs) Understanding the CronJob API The CronJob resource in Kubernetes is designed for time-based job execution. The new API (batch/v1) brings enhancements in reliability and scalability. Use Cases Database Backup Regular database backups are crucial for data integrity. A cron job can be configured to perform database backups at regular intervals, say, daily at midnight. See the following YAML example: YAML apiVersion: batch/v1 kind: CronJob metadata: name: db-backup spec: schedule: "0 0 * * *" jobTemplate: spec: template: spec: restartPolicy: OnFailure volumes: - name: backup-volume hostPath: path: /mnt/backup containers: - name: db-backup image: mysql:5.7 args: - mysqldump - --host=<database host> - --user=root - --password=<database password> - --result-file=/mnt/backup/all-databases.sql - <database name> volumeMounts: - name: backup-volume mountPath: /mnt/backup Explanation of Key Components apiVersion: batch/v1: Specifies the API version. kind: CronJob: Defines the resource type. metadata: Contains the name of the cron job. spec.schedule: Cron format string, here set to run daily at midnight. jobTemplate: Template for the job to be created. containers: name: Name of the container. image: Docker image to use (MySQL 5.7 in this case). args: Commands to execute in the container. Here, it runs mysqldump to backup all databases. result-file=/mnt/backup/all-databases.sql: Redirects the output to a file. restartPolicy: OnFailure: Restart strategy for the container. volumes and volumeMounts: Configures a volume for storing the backup file Automated License Plate Recognition A large commercial parking area requires an efficient system to track vehicles entering and exiting by recognizing their license plates. This scenario outlines a Kubernetes CronJob setup for processing images captured by parking area cameras, using an Automated License Plate Recognition (ALPR) system. See the following YAML snippet: YAML apiVersion: batch/v1 kind: CronJob metadata: name: alpr-job spec: schedule: "*/5 * * * *" # Every 5 minutes jobTemplate: spec: template: spec: containers: - name: alpr-processor image: mycompany/alpr-processor:latest env: - name: IMAGE_SOURCE_DIR value: "/data/camera-feeds" - name: PROCESSED_IMAGE_DIR value: "/data/processed" volumeMounts: - name: camera-data mountPath: "/data" restartPolicy: OnFailure volumes: - name: camera-data persistentVolumeClaim: claimName: camera-data-pvc Explanation of Key Components schedule: "*/5 * * * *": The cron job runs every 5 minutes to process recent images. containers: image: mycompany/alpr-processor:latest: A custom Docker image containing the ALPR software. You can search Docker Hub and replace this with the appropriate container image. env: Environment variables set the paths for the source and processed images. volumeMounts and volumes: A Persistent Volume Claim (PVC) is used to store images from cameras and processed data. Some of the benefits from the above use case can be as following: Entry and Exit Tracking: The system processes images to extract license plate data, providing real-time information on vehicles entering or exiting. Security and Surveillance: Enhanced monitoring of vehicle movement for security purposes. Data Analytics: Accumulate data over time for traffic pattern analysis and parking management optimization. Other Use Cases Report Generation Generate and email system performance reports or business analytics daily or weekly. Cleanup Operations Automatically purge temporary files, logs, or unused resources from your system every night to maintain a clean and efficient environment. Data Synchronization Synchronize data between different environments or systems, like syncing staging database with production every weekend. Certificate Renewal Automate the renewal of SSL/TLS certificates before they expire. Deploy Cron Jobs You can deploy a cron job as show below: kubectl apply -f db-backup-cronjob.yaml To list the jobs fired by the cron jobs, the following command can be used: kubectl get jobs Conclusion Leveraging Kubernetes' new CronJob API allows for efficient and automated management of routine tasks, enhancing both operational efficiency and system reliability. These practical use cases demonstrate how cron jobs can be pivotal in various scenarios, from data management to system maintenance. Disclaimer: This guide is intended for users with a basic understanding of Kubernetes concepts.
Over the past four years, developers have harnessed the power of Quarkus, experiencing its transformative capabilities in evolving Java microservices from local development to cloud deployments. As we stand on the brink of a new era, Quarkus 3 beckons with a promise of even more enhanced features, elevating developer experience, performance, scalability, and seamless cloud integration. In this enlightening journey, let’s delve into the heart of Quarkus 3's integration with virtual threads (Project Loom). You will learn how Quarkus enables you to simplify the creation of asynchronous concurrent applications, leveraging virtual threads for unparalleled scalability while ensuring efficient memory usage and peak performance. Journey of Java Threads You might have some experience with various types of Java threads if you have implemented Java applications for years. Let me remind you real quick how Java threads have been evolving over the last decades. Java threads have undergone significant advancements since their introduction in Java 1.0. The initial focus was on establishing fundamental concurrency mechanisms, including thread management, thread priorities, thread synchronization, and thread communication. As Java matured, it introduced atomic classes, concurrent collections, the ExecutorService framework, and the Lock and Condition interfaces, providing more sophisticated and efficient concurrency tools. Java 8 marked a turning point with the introduction of functional interfaces, lambda expressions, and the CompletableFuture API, enabling a more concise and expressive approach to asynchronous programming. Additionally, the Reactive Streams API standardized asynchronous stream processing and Project Loom introduced virtual threads, offering lightweight threads and improved concurrency support. Java 19 further enhanced concurrency features with structured concurrency constructs, such as Flow and WorkStealing, providing more structured and composable concurrency patterns. These advancements have significantly strengthened Java's concurrency capabilities, making it easier to develop scalable and performant concurrent applications. Java threads continue to evolve, with ongoing research and development focused on improving performance, scalability, and developer productivity in concurrent programming. Virtual threads, generally available (GA) in Java 21, are a revolutionary concurrency feature that addresses the limitations of traditional operating system (OS) threads. OS threads are heavyweight, limited in scalability, and complex to manage, posing challenges for developing scalable and performant concurrent applications. Virtual threads also offer several benefits, such as being a lightweight and efficient alternative, consuming less memory, reducing context-switching overhead, and supporting concurrent tasks. They simplify thread management, improve performance, and enhance scalability, paving the way for new concurrency paradigms and enabling more efficient serverless computing and microservices architectures. Virtual threads represent a significant advancement in Java concurrency, poised to shape the future of concurrent programming. Getting Started With Virtual Threads In general, you need to create a virtual thread using Thread.Builder directly in your Java project using JDK 21. For example, the following code snippet showcases how developers can create a new virtual thread and print a message to the console from the virtual thread. The Thread.ofVirtual() method creates a new virtual thread builder, and the name() method sets the name of the virtual thread to "virtual-thread". Then, the start() method starts the virtual thread and executes the provided Runnable lambda expression, which prints a message to the console. Lastly, the join() method waits for the virtual thread to finish executing before continuing. The System.out.println() statement in the main thread prints a message to the console after the virtual thread has finished executing. Java public class MyVirtualThread { public static void main(String[] args) throws InterruptedException { // Create a new virtual thread using Thread.Builder Thread thread = Thread .ofVirtual() .name("my-vt") .start(() -> { System.out.println("Hello from virtual thread!"); }); // Wait for the virtual thread to finish executing thread.join(); System.out.println("Main thread completed."); } } Alternatively, you can implement the ThreadFactory interface to start a new virtual thread in your Java project with JDK 21. The following code snippet showcases how developers can define a VirtualThreadFactory class that implements the ThreadFactory interface. The newThread() method of this class creates a new virtual thread using the Thread.ofVirtual() method. The name() method of the Builder object is used to set the name of the thread and the factory() method is used to set the ThreadFactory object. Java // Implement a ThreadFactory to start a new virtual thread public class VirtualThreadFactory implements ThreadFactory { private final String namePrefix; public VirtualThreadFactory(String namePrefix) { this.namePrefix = namePrefix; } @Override public Thread newThread(Runnable r) { return Thread.ofVirtual() .name(namePrefix + "-" + r.hashCode()) .factory(this) .build(); } } You might feel it will get more complex when you try to run your actual methods or classes on top of the virtual threads. Luckily, Quarkus enables you to skip the learning curve and execute the existing blocking services on the virtual threads quickly and efficiently. Let’s dive into it. Quarkus Way to the Virtual Thread You just need to keep reminding yourself of two things to run an application on virtual threads. Implement blocking services rather than reactive (or non-blocking) services based on JDK 21. Use @RunOnVirtualThread annotation on top of a method or a class that you want. Here is a code snippet of how Quarkus allows you to run the process() method on a virtual thread. Java @Path("/hello") public class GreetingResource { @GET @Produces(MediaType.TEXT_PLAIN) @RunOnVirtualThread public String hello() { Log.info(Thread.currentThread()); return "Quarkus 3: The Future of Java Microservices with Virtual Threads and Beyond"; } } You can start the Quarkus Dev mode (Live coding) to verify the above sample application. Then, invoke the REST endpoint using the curl command. Shell $ curl http://localhost:8080/hello The output should look like this. Shell Quarkus 3: The Future of Java Microservices with Virtual Threads and Beyond When you take a look at the terminal, you see that Quarkus dev mode is running. You can see that a virtual thread is created to run this application. Shell (quarkus-virtual-thread-0) VirtualThread[#123,quarkus-virtual-thread-0]/runnable@ForkJoinPool-1-worker-1 Try to invoke the endpoint a few more times, and the logs in the terminal should look like this. You learned how Quarkus integrates the virtual thread for Java developers to run blocking applications with a single @RunOnVirtualThread annotation. You should be aware that this annotation is not a silver bullet for all use cases. In the next article, I’ll introduce pitfalls, limitations, and performance test results against reactive applications.
In the dynamic landscape of web development, the choice of an API technology plays a pivotal role in determining the success and efficiency of a project. In this article, we embark on a comprehensive exploration of three prominent contenders: REST, gRPC, and GraphQL. Each of these technologies brings its own set of strengths and capabilities to the table, catering to different use cases and development scenarios. What Is REST? REST API, or Representational State Transfer Application Programming Interface, is a set of architectural principles and conventions for building web services. It provides a standardized way for different software applications to communicate with each other over the Internet. REST is often used in the context of web development to create scalable and maintainable APIs that can be easily consumed by a variety of clients, such as web browsers or mobile applications. Key characteristics of a REST API include: Statelessness: Each request from a client to a server contains all the information needed to understand and process the request. The server does not store any information about the client's state between requests. This enhances scalability and simplifies the implementation on both the client and server sides. Resource-based: REST APIs are centered around resources, which are identified by URLs (Uniform Resource Locators). These resources can represent entities like objects, data, or services. CRUD (Create, Read, Update, Delete) operations are performed on these resources using standard HTTP methods like GET, POST, PUT, and DELETE. Representation: Resources are represented in a format such as JSON (JavaScript Object Notation) or XML (eXtensible Markup Language). Clients can request different representations of a resource, and the server will respond with the data in the requested format. Uniform interface: REST APIs maintain a uniform interface, making it easy for developers to understand and work with different APIs. This uniformity is achieved through a set of constraints, including statelessness, resource-based representation, and standard HTTP methods. Stateless communication: Communication between the client and server is stateless, meaning that each request from the client contains all the information necessary for the server to fulfill that request. The server does not store any information about the client's state between requests. Client-server architecture: REST APIs follow a client-server architecture, where the client and server are independent entities that communicate over a network. This separation allows for flexibility and scalability, as changes to one component do not necessarily affect the other. Cacheability: Responses from the server can be explicitly marked as cacheable or non-cacheable, allowing clients to optimize performance by caching responses when appropriate. REST APIs are widely used in web development due to their simplicity, scalability, and compatibility with the HTTP protocol. They are commonly employed to enable communication between different components of a web application, including front-end clients and back-end servers, or to facilitate integration between different software systems. Pros and Cons of REST REST has several advantages that contribute to its widespread adoption in web development. One key advantage is its simplicity, as RESTful APIs are easy to understand and implement. This simplicity accelerates the development process and facilitates integration between different components of a system. The statelessness of RESTful communication allows for easy scalability, as each request from the client contains all the necessary information, and servers don't need to maintain client state between requests. REST's flexibility, compatibility with various data formats (commonly JSON), and support for caching enhance its overall performance. Its well-established nature and support from numerous tools and frameworks make REST a popular and accessible choice for building APIs. However, REST does come with certain disadvantages. One notable challenge is the potential for over-fetching or under-fetching of data, where clients may receive more information than needed or insufficient data, leading to additional requests. The lack of flexibility in data retrieval, especially in scenarios where clients require specific data combinations, can result in inefficiencies. Additionally, while REST is excellent for stateless communication, it lacks built-in support for real-time features, requiring developers to implement additional technologies or workarounds for immediate data updates. Despite these limitations, the advantages of simplicity, scalability, and widespread support make REST a robust choice for many web development projects. What Is gPRC? gRPC, which stands for "gRPC Remote Procedure Calls," is an open-source RPC (Remote Procedure Call) framework developed by Google. It uses HTTP/2 as its transport protocol and Protocol Buffers (protobuf) as the interface description language. gRPC facilitates communication between client and server applications, allowing them to invoke methods on each other as if they were local procedures, making it a powerful tool for building efficient and scalable distributed systems. Key features of gRPC include: Performance: gRPC is designed to be highly efficient, leveraging the capabilities of HTTP/2 for multiplexing multiple requests over a single connection. It also uses Protocol Buffers, a binary serialization format, which results in faster and more compact data transmission compared to traditional text-based formats like JSON. Language agnostic: gRPC supports multiple programming languages, enabling developers to build applications in languages such as Java, C++, Python, Go, Ruby, and more. This language-agnostic nature promotes interoperability between different components of a system. IDL (Interface Definition Language): gRPC uses Protocol Buffers as its IDL for defining the service methods and message types exchanged between the client and server. This provides a clear and structured way to define APIs, allowing for automatic code generation in various programming languages. Bidirectional streaming: One of gRPC's notable features is its support for bidirectional streaming. This means that both the client and server can send a stream of messages to each other over a single connection, providing flexibility in communication patterns. Code generation: gRPC generates client and server code based on the service definition written in Protocol Buffers. This automatic code generation simplifies the development process and ensures that the client and server interfaces are in sync. Strong typing: gRPC uses strongly typed messages and service definitions, reducing the chances of runtime errors, and making the communication between services more robust. Support for authentication and authorization: gRPC supports various authentication mechanisms, including SSL/TLS for secure communication. It also allows for the implementation of custom authentication and authorization mechanisms. gRPC is particularly well-suited for scenarios where high performance, scalability, and efficient communication between distributed systems are critical, such as in microservices architectures. Its use of modern protocols and technologies makes it a compelling choice for building complex and scalable applications. Pros and Cons of gPRC gRPC presents several advantages that contribute to its popularity in modern distributed systems. One key strength is its efficiency, as it utilizes the HTTP/2 protocol, enabling multiplexing of multiple requests over a single connection and reducing latency. This efficiency, combined with the use of Protocol Buffers for serialization, results in faster and more compact data transmission compared to traditional REST APIs, making gRPC well-suited for high-performance applications. The language-agnostic nature of gRPC allows developers to work with their preferred programming languages, promoting interoperability in heterogeneous environments. The inclusion of bidirectional streaming and strong typing through Protocol Buffers further enhances its capabilities, offering flexibility and reliability in communication between client and server components. While gRPC offers substantial advantages, it comes with certain challenges. One notable drawback is the learning curve associated with adopting gRPC, particularly for teams unfamiliar with Protocol Buffers and the concept of remote procedure calls. Debugging gRPC services can be more challenging due to the binary nature of Protocol Buffers, requiring specialized tools and knowledge for effective troubleshooting. Additionally, the maturity of the gRPC ecosystem may vary across different languages and platforms, potentially impacting the availability of third-party libraries and community support. Integrating gRPC into existing systems or environments that do not fully support HTTP/2 may pose compatibility challenges, requiring careful consideration before migration. Despite these challenges, the efficiency, flexibility, and performance benefits make gRPC a compelling choice for certain types of distributed systems. What Is GraphQL? GraphQL is a query language for APIs (Application Programming Interfaces) and a runtime for executing those queries with existing data. It was developed by Facebook in 2012 and later open-sourced in 2015. GraphQL provides a more efficient, powerful, and flexible alternative to traditional REST APIs by allowing clients to request only the specific data they need. Key features of GraphQL include: Declarative data fetching: Clients can specify the structure of the response they need, including nested data and relationships, in a single query. This eliminates over-fetching and under-fetching of data, ensuring that clients precisely receive the information they request. Single endpoint: GraphQL APIs typically expose a single endpoint, consolidating multiple RESTful endpoints into one. This simplifies the API surface and allows clients to request all the required data in a single query. Strong typing and schema: GraphQL APIs are defined by a schema that specifies the types of data that can be queried and the relationships between them. This schema provides a clear contract between clients and servers, enabling strong typing and automatic validation of queries. Real-time updates (subscriptions): GraphQL supports real-time data updates through a feature called subscriptions. Clients can subscribe to specific events, and the server will push updates to the client when relevant data changes. Introspection: GraphQL APIs are self-documenting. Clients can query the schema itself to discover the types, fields, and relationships available in the API, making it easier to explore and understand the data model. Batched queries: Clients can send multiple queries in a single request, reducing the number of network requests and improving efficiency. Backend aggregation: GraphQL allows the backend to aggregate data from multiple sources, such as databases, microservices, or third-party APIs, and present it to the client in a unified way. GraphQL is often used in modern web development, particularly in single-page applications (SPAs) and mobile apps, where optimizing data transfer and minimizing over-fetching are crucial. It has gained widespread adoption and is supported by various programming languages and frameworks, both on the client and server sides. Deciding the Right API Technology Choosing between REST, gRPC, and GraphQL depends on the specific requirements and characteristics of your project. Each technology has its strengths and weaknesses, making them more suitable for certain use cases. Here are some considerations for when to choose REST, gRPC, or GraphQL: Choose REST when: Simplicity is key: REST is straightforward and easy to understand. If your project requires a simple and intuitive API, REST might be the better choice. Statelessness is sufficient: If statelessness aligns well with your application's requirements and you don't need advanced features like bidirectional streaming, REST is a good fit. Widespread adoption and compatibility: If you need broad compatibility with various clients, platforms, and tooling, REST is well-established and widely supported. Choose gRPC when: High performance is critical: gRPC is designed for high-performance communication, making it suitable for scenarios where low latency and efficient data transfer are crucial, such as microservices architectures. Strong typing is important: If you value strong typing and automatic code generation for multiple programming languages, gRPC's use of Protocol Buffers can be a significant advantage. Bidirectional streaming is needed: For applications that require bidirectional streaming, real-time updates, and efficient communication between clients and servers, gRPC provides a robust solution. Choose GraphQL when: Flexible data retrieval is required: If your application demands flexibility in data retrieval and allows clients to specify the exact data they need, GraphQL's query language provides a powerful and efficient solution. Reducing over-fetching and under-fetching is a priority: GraphQL helps eliminate over-fetching and under-fetching of data by allowing clients to request only the specific data they need. This is beneficial in scenarios where optimizing data transfer is crucial. Real-time updates are essential: If real-time features and the ability to subscribe to data updates are critical for your application (e.g., chat applications, live notifications), GraphQL's support for subscriptions makes it a strong contender. Ultimately, the choice between REST, gRPC, and GraphQL should be based on a careful evaluation of your project's requirements, existing infrastructure, and the specific features offered by each technology. Additionally, consider factors such as developer familiarity, community support, and ecosystem maturity when making your decision. It's also worth noting that hybrid approaches, where different technologies are used for different parts of an application, can be viable in certain scenarios. Conclusion The choice between REST, gRPC, and GraphQL is a nuanced decision that hinges on the specific requirements and objectives of a given project. REST, with its simplicity and widespread adoption, remains a solid choice for scenarios where ease of understanding and compatibility are paramount. Its statelessness and broad support make it an excellent fit for many web development projects. On the other hand, gRPC emerges as a powerful contender when high performance and efficiency are critical, particularly in microservices architectures. Its strong typing, bidirectional streaming, and automatic code generation make it well-suited for applications demanding low-latency communication and real-time updates. Meanwhile, GraphQL addresses the need for flexible data retrieval and the elimination of over-fetching and under-fetching, making it an optimal choice for scenarios where customization and optimization of data transfer are essential, especially in applications requiring real-time features. Ultimately, the decision should be guided by a careful assessment of project requirements, developer expertise, and the specific features offered by each technology, recognizing that a hybrid approach may offer a pragmatic solution in certain contexts.
In today's world of distributed systems and microservices, it is crucial to maintain consistency. Microservice architecture is considered almost a standard for building modern, flexible, and reliable high-loaded systems. But at the same time introduces additional complexities. Monolith vs Microservices In monolithic applications, consistency can be achieved using transactions. Within a transaction, we can modify data in multiple tables. If an error occurred during the modification process, the transaction would roll back and the data would remain consistent. Thus consistency was achieved by the database tools. In a microservice architecture, things get much more complicated. At some point, we will have to change data not only in the current microservice but also in other microservices. Imagine a scenario where a user interacts with a web application and creates an order on the website. When the order is created, it is necessary to reduce the number of items in stock. In a monolithic application, this could look like the following: In a microservice architecture, such tables can change within different microservices. When creating an order, we need to call another service using, for example, REST or Kafka. But there are many problems here: the request may fail, the network or the microservice may be temporarily unavailable, the microservice may stop immediately after creating a record in the orders table and the message will not be sent, etc. Transactional Outbox One solution to this problem is to use the transactional outbox pattern. We can create an order and a record in the outbox table within one transaction, where we will add all the necessary data for a future event. A specific handler will read this record and send the event to another microservice. This way we ensure that the event will be sent if we have successfully created an order. If the network or microservice is unavailable, then the handler will keep trying to send the message until it receives a successful response. This will result in eventual consistency. It is worth noting here that it is necessary to support idempotency because, in such architectures, request processing may be duplicated. Implementation Let's consider an example of implementation in a Spring Boot application. We will use a ready solution transaction-outbox. First, let's start PostgreSQL in Docker: Shell docker run -d -p 5432:5432 --name db \ -e POSTGRES_USER=admin \ -e POSTGRES_PASSWORD=password \ -e POSTGRES_DB=demo \ postgres:12-alpine Add a dependency to build.gradle: Groovy implementation 'com.gruelbox:transactionoutbox-spring:5.3.370' Declare the configuration: Java @Configuration @EnableScheduling @Import({ SpringTransactionOutboxConfiguration.class }) public class TransactionOutboxConfig { @Bean public TransactionOutbox transactionOutbox(SpringTransactionManager springTransactionManager, SpringInstantiator springInstantiator) { return TransactionOutbox.builder() .instantiator(springInstantiator) .initializeImmediately(true) .retentionThreshold(Duration.ofMinutes(5)) .attemptFrequency(Duration.ofSeconds(30)) .blockAfterAttempts(5) .transactionManager(springTransactionManager) .persistor(Persistor.forDialect(Dialect.POSTGRESQL_9)) .build(); } } Here we specify how many attempts should be made in case of unsuccessful request sending, the interval between attempts, etc. For the functioning of a separate thread that will parse records from the outbox table, we need to call outbox.flush() periodically. For this purpose, let's declare a component: Java @Component @AllArgsConstructor public class TransactionOutboxWorker { private final TransactionOutbox transactionOutbox; @Scheduled(fixedDelay = 5000) public void flushTransactionOutbox() { transactionOutbox.flush(); } } The execution time of flush should be chosen according to your requirements. Now we can implement the method with business logic. We need to create an Order in the database and send the event to another microservice. For demonstration purposes, I will not implement the actual call but will simulate the error of sending the event by throwing an exception. The method itself should be marked @Transactional, and the event sending should be done not directly, but using the TransactionOutbox object: Java @Service @AllArgsConstructor @Slf4j public class OrderService { private OrderRepository repository; private TransactionOutbox outbox; @Transactional public String createOrderAndSendEvent(Integer productId, Integer quantity) { String uuid = UUID.randomUUID().toString(); repository.save(new OrderEntity(uuid, productId, quantity)); outbox.schedule(getClass()).sendOrderEvent(uuid, productId, quantity); return uuid; } void sendOrderEvent(String uuid, Integer productId, Integer quantity) { log.info(String.format("Sending event for %s...", uuid)); if (ThreadLocalRandom.current().nextBoolean()) throw new RuntimeException(); log.info(String.format("Event sent for %s", uuid)); } } Here randomly the method may throw an exception. However, the key feature is that this method is not called directly, and the call information is stored in the Outbox table within a single transaction. Let's start the service and execute the query: Shell curl --header "Content-Type: application/json" \ --request POST \ --data '{"productId":"10","quantity":"2"}' \ http://localhost:8080/order {"id":"6a8e2960-8e94-463b-90cb-26ce8b46e96c"} If the method is successful, the record is removed from the table, but if there is a problem, we can see the record in the table: Shell docker exec -ti <CONTAINER ID> bash psql -U admin demo psql (12.16) Type "help" for help. demo=# \x Expanded display is on. demo=# SELECT * FROM txno_outbox; -[ RECORD 1 ]---+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ id | d0b69f7b-943a-44c9-9e71-27f738161c8e invocation | {"c":"orderService","m":"sendOrderEvent","p":["String","Integer","Integer"],"a":[{"t":"String","v":"6a8e2960-8e94-463b-90cb-26ce8b46e96c"},{"t":"Integer","v":10},{"t":"Integer","v":2}]} nextattempttime | 2023-11-19 17:59:12.999 attempts | 1 blocked | f version | 1 uniquerequestid | processed | f lastattempttime | 2023-11-19 17:58:42.999515 Here we can see the parameters of the method call, the time of the next attempt, the number of attempts, etc. According to your settings, the handler will try to execute the request until it succeeds or until it reaches the limit of attempts. This way, even if our service restarts (which is considered normal for cloud-native applications), we will not lose important data about the external service call, and eventually the message will be delivered to the recipient. Conclusion Transactional outbox is a powerful solution for addressing data consistency issues in distributed systems. It provides a reliable and organized approach to managing transactions between microservices. This greatly reduces the risks associated with data inconsistency. We have examined the fundamental principles of the transactional outbox pattern, its implementation, and its benefits in maintaining a coherent and synchronized data state. The project code is available on GitHub.
In today’s evolving technological landscape, the shift from monolithic architectures to microservices is a strategic move for many businesses. This is particularly relevant in the domain of reimbursement calculation systems. As I mentioned in my previous article Part 1, let's explore how such a transition can be effectively managed. The Monolithic Challenge Imagine a scenario where you have a large-scale, monolithic system - possibly a bulky C# console application or an extensive SQL Server stored procedure. This system is tasked with performing reimbursement calculations, typically running overnight through a batch process scheduled in SQL Server. While functional, this monolithic approach often leads to challenges in scalability, flexibility, and maintenance. Moving to Microservices The objective of migrating to microservices is to decompose this large, complex system into smaller, more manageable components. The transition to a microservices architecture aims to leverage the cloud's advantages, including scalability, resource optimization, and cost-effectiveness. Steps for Migration 1. Understanding the System Begin by defining the data models from the existing monolithic application to understand its workflow, dependencies, and key components of the reimbursement calculation process. Source data for this system is thru 837 File which is a standardized electronic format for healthcare claim information. This file is extracted and data is generally loaded into a database through another loading process for the purpose of reimbursement calculation. For example, a few data models from the 837 file might look as below: C# public class Patient { public string Name { get; set; } public DateTime DateOfBirth { get; set; } public string Address { get; set; } public string Gender { get; set; } public string PatientId { get; set; } } public class Provider { public string Name { get; set; } public string Address { get; set; } public string NPI { get; set; } public string TaxID { get; set; } public string RenderingProvider { get; set; } } public class Claim { public string ControlNumber { get; set; } public DateTime ServiceFromDate { get; set; } public DateTime ServiceToDate { get; set; } public string TypeOfBill { get; set; } public string AdmissionType { get; set; } public string DischargeStatus { get; set; } public List<string> DiagnosisCodes { get; set; } public List<string> ProcedureCodes { get; set; } } public class Insurance { public string PayerName { get; set; } public string PayerAddress { get; set; } public string PayerId { get; set; } public string SubscriberInformation { get; set; } public string SubscriberId { get; set; } public string CoordinationOfBenefitsData { get; set; } } public class ServiceLine { public string RevenueCode { get; set; } public DateTime ServiceDate { get; set; } public int ServiceUnits { get; set; } public decimal ServiceCharges { get; set; } public List<string> ServiceModifiers { get; set; } } 2. Identifying Microservices Break down the monolithic process into smaller, logically separated services. Each microservice should represent a specific aspect of the reimbursement calculation, such as input validation, calculation logic, and output generation. In many cases, a healthcare reimbursement system can involve multiple microservices working together to provide end-to-end functionality. Here are some microservices that might be part of a comprehensive healthcare reimbursement system: For demonstration purposes, I will provide a simplified implementation of the Reimbursement Calculation Service. It is assumed that patient information, procedure details, and fee schedule data are retrieved from their respective microservices and passed as inputs to this service, Reimbursement.web layer: C# using Microsoft.AspNetCore.Mvc; using Reimbursement.Service; namespace Reimbursement.Controllers { [Route("api/[controller]")] [ApiController] public class ReimbursementController : ControllerBase { private IReimbursementService _reimbursementService; public ReimbursementController(IReimbursementService reimbursementService) { _reimbursementService = reimbursementService; } [HttpPost("calculate")] public ActionResult<decimal> CalculateExpectedReimbursement(Patient patient, Procedure procedure, FeeSchedule feeSchedule) { try { decimal expectedReimbursement = _reimbursementService.CalculateExpectedReimbursement(patient, procedure, feeSchedule); return Ok(expectedReimbursement); } catch (Exception ex) { return StatusCode(500, $"Internal server error: {ex.Message}"); } } } } Reimbursement service layer: C# using System; namespace Reimbursement.Service { public class ReimbursementService : IReimbursementService { public decimal CalculateExpectedReimbursement(Patient patient, Procedure procedure, FeeSchedule feeSchedule) { // Check if the patient and procedure exist if (patient == null || procedure == null) { throw new ArgumentNullException("Patient and Procedure must be provided."); } // Check if the feeSchedule exists if (feeSchedule == null) { throw new ArgumentNullException("FeeSchedule must be provided."); } // Calculate the expected reimbursement decimal expectedReimbursement = feeSchedule.Fee; // Basic reimbursement logic // You can add more complex reimbursement calculations here based on patient data and rules return expectedReimbursement; } } } The exact composition and architecture of microservices in a healthcare reimbursement system may vary based on the specific needs and scale of the application. The services listed above are examples of components that can be part of such a system, and they may interact with each other through APIs or message queues to perform end-to-end reimbursement processes. 3. Batch Processing in the Cloud Adapt the overnight batch processing to the cloud environment. This could involve leveraging cloud-native services for scheduled tasks, ensuring that the process is reliable and scalable. CalculationService can also be triggered through the user interface manually in case users need to rerun for specific accounts only so that this service can be reused in places other than batch processing. Conclusion Migrating a complex, monolithic reimbursement calculation system to microservices and deploying it in the cloud is a transformative step. This approach not only modernizes the system but also brings significant benefits in terms of scalability, resource utilization, and cost savings, aligning the system with modern cloud capabilities and business objectives.
The U.S. Securities and Exchange Commission (SEC) recently announced its new rules for public companies regarding cybersecurity risk management, strategy, governance, and incident exposure. Some requirements apply to this year—for example, disclosures for fiscal years ending December 15, 2023, or later have new annual reporting requirements. As a result, organizations are wondering about how these new rules impact them. In this post, we’ll help unpack the new rules, what they mean to you, and what your DevOps and DevSecOps teams might need to implement in response. Understanding the SEC Announcement In the press release, SEC Chair Gary Gensler has a quote that helps to summarize why the new rules are being implemented: “Currently, many public companies provide cybersecurity disclosure to investors. I think companies and investors alike, however, would benefit if this disclosure were made in a more consistent, comparable, and decision-useful way. Through helping to ensure that companies disclose material cybersecurity information, today’s rules will benefit investors, companies, and the markets connecting them.” Section I of the SEC’s Final Rule document helps us further understand the impetus for these new rules. Let’s summarize some of the key points: Cybersecurity incidents have been reported inconsistently. Cybersecurity incidents are also likely underreported. Economic activity is increasingly dependent on electronic systems that are susceptible to cyber risk. The frequency and financial impacts of cyberattacks are on the rise. With that in mind, it makes sense why the SEC would want to standardize how incidents are reported. Let’s take a closer look at some of the new rules’ specifics. What Is a “Material Cybersecurity Incident”? The new rules establish requirements for reporting material cybersecurity incidents. For tech folks, a phrase like “material cybersecurity incident” can be tough legalese to decipher. It’s clear that a breach compromising millions of sensitive records or costing tens of millions of dollars is material, but at what point is a breach not considered material? Fortunately, we have some precedents to help guide our interpretation. The concept of “materiality” has long been important regarding SEC regulations. Typically, the key characteristics of “materiality” are summarized by these two quotes from Justice Thurgood Marshall in a 1976 opinion indicating a fact is material if: There is a “substantial likelihood that a reasonable shareholder would consider it important in deciding how to vote.” A reasonable investor would see the fact as “having significantly altered the ‘total mix’ of information made available.” Note that no specific dollar amount makes an incident material. Additionally, multiple connected immaterial incidents can become material in context. One example that the SEC’s Final Rule cites includes a single threat actor engaging in multiple smaller but continuous attacks against an organization. For “cybersecurity incident,” the SEC defines the term for us: “an unauthorized occurrence on or conducted through a registrant’s information systems that jeopardizes the confidentiality, integrity, or availability of a registrant’s information systems or any information residing therein.” What Is the Timeframe for Reporting an Incident? Once an incident is deemed “material,” affected organizations have to file SEC Form 8-K within four business days. Some exceptions to this timeframe exist, and they’re related to national security reasons and a provision for omitting information related to national defense and foreign policy. Which Organizations Are Affected? Public companies are affected by the new SEC rules. That includes foreign private issuers (FPIs), a type of company registered outside of the U.S. but doing significant business inside the U.S. How Does an Organization Disclose This Information? The reporting requirements in the new SEC rules call out different forms that organizations must complete to disclose relevant information. They even include a requirement for disclosures to use the Inline eXtensible Business Reporting Language (Inline XBRL). The table below breaks down the key forms related to the new SEC rules and when required. Note that the forms aren’t new, but the new SEC rules have added new requirements related to cybersecurity disclosures. Form What is the form used for as it relates to the new rules? When is it required? 8-K To disclose information related to any material cybersecurity incident To describe the material aspects of the reported incident To describe the scope, nature, and timing of the incident To describe the likely impact, including any impact on finances and operations Item 1.05 of Form 8-K must be filed within four business days of a cybersecurity incident being deemed material 10-K To fulfill annual reporting requirements from Regulation S-K Item 106 To disclose processes for assessing, identifying, and managing cybersecurity risk To disclose effects or likely effects of cybersecurity threats and past incidents Describe the board of directors’ role in cybersecurity risk oversight Describe the role of management in the assessment and management of cybersecurity threats Annually 6-K Similar to Form 8-K, but for foreign private issuers After a material cybersecurity incident 20-F Similar to Form 10-K, but for foreign private issuers Annually How Is This Different From Other Standards in the Past? For publicly traded companies, rules and regulations are nothing new. Many organizations already face strict reporting requirements related to regulations such as HIPAA, PCI DSS, and SOX. Some of the most meaningful changes for publicly traded organizations include: Standardization in reporting requirements: Previously, cybersecurity incidents were reported with varying levels of detail and frequency. The new SEC rules standardize how (based on the forms) and when an organization must report incidents. Well-defined annual reporting updates: Publicly traded organizations now must report on cybersecurity practices and impact annually through Form 10-K or Form 20-F. Now, let’s get down to brass tacks. If you’re a CISO or a part of your organization’s Security, DevSecOps, or Governance Risk and Compliance team, what are the practical implications of these new rules? How Do the New SEC Rules Affect Your Cybersecurity Measures? The new SEC rules for public companies effectively create cybersecurity, disclosure, and governance requirements that organizations must address in their internal processes and policies. For example, the new rules mean that affected organizations must quickly detect and analyze cybersecurity incidents. Incident response and analysis capabilities need to be mature enough to enable disclosure of the “nature, scope, and timing” of the event for adequate disclosure. The emphasis on board and management involvement also creates governance requirements for organizations. This may increase C-suite support for cybersecurity initiatives that may have otherwise languished. As a result, this increased emphasis on governance may drive an increased focus on leveraging the right tactics and tools to enable effective detection, prevention, and disclosure of cybersecurity threats. What Tooling Can Help You Adhere to the New Rules? From a tooling perspective, the new SEC rules should drive organizations to focus on the following: Incident detection and prevention, include the ability to identify and mitigate vulnerabilities before they become full-blown incidents. Incident response, which covers the ability to recover from cybersecurity incidents and capture the relevant data to disclose its “nature, scope, and timing” to shareholders. Let’s look at some of the tools and practices most relevant to enabling incident detection, prevention, and response. Continuous Monitoring Continuous monitoring of IT infrastructure is essential to threat detection, root cause analysis, and incident response. With continuous monitoring platforms, enterprises can verify that adequate security controls are in place. That way, they can detect anomalies and improve MTTR if an incident occurs. SIEM A SIEM tool aggregates log data across an environment, enabling alerting, reporting, analysis, and data retention capabilities. When coupled with effective logging, SIEM platforms provide many of the capabilities organizations need to comply with the new SEC cybersecurity rules. For example, an SIEM platform can provide continuous log data monitoring, correlating alerts, and security events from across all of your enterprise security tools. It will help you investigate and respond to threats quickly. Some Cloud SIEM platforms can even extend traditional SIEM capabilities for modern enterprises with user and entity behavior analytics (UEBA) to detect unknown threats. Why Logging Is Essential for Cybersecurity Log management is a critical part of any organization’s cybersecurity toolkit. Logging helps enterprises capture and retain key security and compliance data, and it enables the alerting and analytics capabilities of tools such as intrusion prevention or intrusion detection systems (IPS/IDS) and SIEM platforms. Robust log centralization and management tools are essential for your cybersecurity posture. And, logging isn’t just relevant to the new SEC rules. Guidelines, requirements, and frameworks (such as FedRAMP, PCI DSS, ISO 27001, HIPAA, and GLBA) may include some logging and data retention requirements for an organization. Conclusion The new SEC rules help standardize cybersecurity incident disclosures and emphasize the importance of governance in addressing cybersecurity risk. For publicly traded organizations, these rules add specificity and structure to handling cybersecurity incidents and reporting on cybersecurity posture. The right tools, specifically platforms that enable effective logging and incident response, are essential to tying together a cybersecurity strategy that mitigates risk and enables adherence to the new rules.