Discover how Kubernetes continues to shape the industry as developers drive innovation and prepare for the future of K8s.
Observability and performance monitoring: DZone's final 2024 Trend Report survey is open! We'd love to hear about your experience.
Java is an object-oriented programming language that allows engineers to produce software for multiple platforms. Our resources in this Zone are designed to help engineers with Java program development, Java SDKs, compilers, interpreters, documentation generators, and other tools used to produce a complete application.
Java Concurrency: The Happens-Before Guarantee
How To Think Simple In Java
Despite being nearly 30 years old, the Java platform remains consistently among the top three most popular programming languages. This enduring popularity can be attributed to the Java Virtual Machine (JVM), which abstracts complexities such as memory management and compiles code during execution, enabling unparalleled internet-level scalability. Java's sustained relevance is also due to the rapid evolution of the language, its libraries, and the JVM. Java Virtual Threads, introduced in Project Loom, which is an initiative by the OpenJDK community, represent a groundbreaking change in how Java handles concurrency. The Complete Java Coder Bundle.* *Affiliate link. See Terms of Use. Exploring the Fabric: Unveiling Threads A thread is the smallest schedulable unit of processing, running concurrently and largely independently of other units. It's an instance of java.lang.Thread. There are two types of threads: platform threads and virtual threads. A platform thread is a thin wrapper around an operating system (OS) thread, running Java code on its underlying OS thread for its entire lifetime. Consequently, the number of platform threads is limited by the number of OS threads. These threads have large stacks and other OS-managed resources, making them suitable for all task types but potentially limited in number. Virtual threads in Java, unlike platform threads, aren't tied to specific OS threads but still execute on them. When a virtual thread encounters a blocking I/O operation, it pauses, allowing the OS thread to handle other tasks. Similar to virtual memory, where a large virtual address space maps to limited RAM, Java's virtual threads map many virtual threads to fewer OS threads. They're ideal for tasks with frequent I/O waits but not for sustained CPU-intensive operations. Hence virtual threads are lightweight threads that simplify the development, maintenance, and debugging of high-throughput concurrent applications. Comparing the Threads of Fabric: Virtual vs. Platform Let’s compare platform threads with virtual threads to understand their differences better. Crafting Virtual Threads Creating Virtual Threads Using Thread Class and Thread.Builder Interface The example below creates and starts a virtual thread that prints a message. It uses the join method to ensure the virtual thread completes before the main thread terminates, allowing you to see the printed message. Java Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello World!! I am Virtual Thread")); thread.join(); The Thread.Builder interface allows you to create threads with common properties like thread names. The Thread.Builder.OfPlatform subinterface creates platform threads, while Thread.Builder.OfVirtual creates virtual threads. Here’s an example of creating a virtual thread named "MyVirtualThread" using the Thread.Builder interface: Java Thread.Builder builder = Thread.ofVirtual().name("MyVirtualThread"); Runnable task = () -> { System.out.println("Thread running"); }; Thread t = builder.start(task); System.out.println("Thread name is: " + t.getName()); t.join(); Creating and Running a Virtual Thread Using Executors.newVirtualThreadPerTaskExecutor() Method Executors allow you to decouple thread management and creation from the rest of your application. In the example below, an ExecutorService is created using the Executors.newVirtualThreadPerTaskExecutor() method. Each time ExecutorService.submit(Runnable) is called, a new virtual thread is created and started to execute the task. This method returns a Future instance. It's important to note that the Future.get() method waits for the task in the thread to finish. As a result, this example prints a message once the virtual thread's task is completed. Java try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) { Future<?> future = myExecutor.submit(() -> System.out.println("Running thread")); future.get(); System.out.println("Task completed"); // ... Is Your Fabric Lightweight With Virtual Threads? Memory Program 1: Create 10,000 Platform Threads Java public class PlatformThreadMemoryAnalyzer { private static class MyTask implements Runnable { @Override public void run() { try { // Sleep for 10 minutes Thread.sleep(600000); } catch (InterruptedException e) { System.err.println("Interrupted Exception!!"); } } } public static void main(String args[]) throws Exception { // Create 10000 platform threads int i = 0; while (i < 10000) { Thread myThread = new Thread(new MyTask()); myThread.start(); i++; } Thread.sleep(600000); } } Program 2: Create 10,000 Virtual Threads Java public class VirtualThreadMemoryAnalyzer { private static class MyTask implements Runnable { @Override public void run() { try { // Sleep for 10 minutes Thread.sleep(600000); } catch (InterruptedException e) { System.err.println("Interrupted Exception!!"); } } } public static void main(String args[]) throws Exception { // Create 10000 virtual threads int i = 0; while (i < 10000) { Thread.ofVirtual().start(new Task()); i++; } Thread.sleep(600000); } } Executed both programs simultaneously in a RedHat VM. Configured the thread stack size to be 1mb (by passing JVM argument -Xss1m). This argument indicates that every thread in this application should be allocated 1mb of stack size. Below is the top command output of the threads running. You can notice that the virtual threads only occupies 7.8mb (i.e., 7842364 bytes), whereas the platform threads program occupies 19.2gb. This clearly indicates that virtual threads consume comparatively much less memory. Thread Creation Time Program 1: Launches 10,000 platform threads Java public class PlatformThreadCreationTimeAnalyzer { private static class Task implements Runnable { @Override public void run() { System.out.println("Hello! I am a Platform Thread"); } } public static void main(String[] args) throws Exception { long startTime = System.currentTimeMillis(); for (int counter = 0; counter < 10_000; ++counter) { new Thread(new Task()).start(); } System.out.print("Platform Thread Creation Time: " + (System.currentTimeMillis() - startTime)); } } Program 2: Launches 10,000 virtual threads Java public class VirtualThreadCreationTimeAnalyzer { private static class Task implements Runnable { @Override public void run() { System.out.println("Hello! I am a Virtual Thread"); } } public static void main(String[] args) throws Exception { long startTime = System.currentTimeMillis(); for (int counter = 0; counter < 10_000; ++counter) { Thread.startVirtualThread(new Task()); } System.out.print("Virtual Thread Creation Time: " + (System.currentTimeMillis() - startTime)); } } Below is the table that summarizes the execution time of these two programs: Virtual Threads Platform Threads Execution Time 84 ms 346 ms You can see that the virtual Thread took only 84 ms to complete, whereas the Platform Thread took almost 346 ms. It’s because platform threads are more expensive to create. Because whenever a platform needs to be created an operating system thread needs to be allocated to it. Creating and allocating an operating system thread is not a cheap operation. Reweaving the Fabric: Applications of Virtual Threads Virtual threads can significantly benefit various types of applications, especially those requiring high concurrency and efficient resource management. Here are a few examples: Web servers: Handling a large number of simultaneous HTTP requests can be efficiently managed with virtual threads, reducing the overhead and complexity of traditional thread pools. Microservices: Microservices often involve a lot of I/O operations, such as database queries and network calls. Virtual threads can handle these operations more efficiently. Data processing: Applications that process large amounts of data concurrently can benefit from the scalability of virtual threads, improving throughput and performance. Weaving Success: Avoiding Pitfalls To make the most out of virtual threads, consider the following best practices: Avoid synchronized blocks/methods: When using virtual threads with synchronized blocks, they may not relinquish control of the underlying OS thread when blocked, limiting their benefits. Avoid thread pools for virtual threads: Virtual threads are meant to be used without traditional thread pools. The JVM manages them efficiently, and thread pools can introduce unnecessary complexity. Reduce ThreadLocal usage: Millions of virtual threads with individual ThreadLocal variables can rapidly consume Java heap memory. Wrapping It Up Virtual threads in Java are threads implemented by the Java runtime, not the operating system. Unlike traditional platform threads, virtual threads can scale to a high number — potentially millions — within the same Java process. This scalability allows them to efficiently handle server applications designed in a thread-per-request style, improving concurrency, throughput, and hardware utilization. Developers familiar with java.lang.Thread since Java SE 1.0 can easily use virtual threads, as they follow the same programming model. However, practices developed to manage the high cost of platform threads are often counterproductive with virtual threads, requiring developers to adjust their approach. This shift in thread management encourages a new perspective on concurrency. "Hello, world? Hold on, I’ll put you on hold, spawn a few more threads, and get back to you" Happy coding. :)
If you're a Java software developer and you weren't living on the planet Mars during these last years, then you certainly know what Quarkus is. And just in case you don't, you may find it out here. With Quarkus, the field of enterprise cloud-native applications development has never been so comfortable and it never took advantage of such a friendly and professional working environment. The Internet abounds with posts and articles explaining why and how Quarkus is a must for the enterprise, cloud-native software developer. And of course, CDK applications aren't on the sidelines: on the opposite, they can greatly take advantage of the Quarkus features to become smaller, faster, and more aligned with requirements nowadays. CDK With Quarkus Let's look at our first CDK with Quarkus example in the code repository. Go to the Maven module named cdk-quarkus and open the file pom.xml to see how to combine specific CDK and Quarkus dependencies and plugins. XML ... <dependency> <groupId>io.quarkus.platform</groupId> <artifactId>quarkus-bom</artifactId> <version>${quarkus.platform.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>io.quarkiverse.amazonservices</groupId> <artifactId>quarkus-amazon-services-bom</artifactId> <version>${quarkus-amazon-services.version}</version> <type>pom</type> <scope>import</scope> </dependency> ... In addition to the aws-cdk-lib artifact which represents the CDK API library and is inherited from the parent Maven module, the dependencies above are required in order to develop CDK Quarkus applications. The first one, quarkus-bom, is the Bill of Material (BOM) which includes all the other required Quarkus artifacts. Here, we're using Quarkus 3.11 which is the most recent release as of this writing. The second one is the BOM of the Quarkus extensions required to interact with AWS services. Another mandatory requirement of Quarkus applications is the use of the quarkus-maven-plugin which is responsible for running the build and augmentation process. Let's recall that as opposed to more traditional frameworks like Spring or Jakarta EE where the application's initialization and configuration steps happen at the runtime, Quarkus performs them at build time, in a specific phase called "augmentation." Consequently, Quarkus doesn't rely on Java introspection and reflection, which is one of the reasons it is much faster than Spring, but needs to use the jandex-maven-plugin to build an index helping to discover annotated classes and beans in external modules. This is almost all as far as the Quarkus master POM is concerned. Let's look now at the CDK submodule. But first, we need to recall that, in order to synthesize and deploy a CDK application, we need a specific working environment defined by the cdk.json file. Hence, trying to use CDK commands in a project not having at its root this file will fail. One of the essential functions of the cdk.json file aims to define how to run the CDK application. By default, the cdk init app --language java command, used to scaffold the project's skeleton, will generate the following JSON statement: JSON ... "app": "mvn -e -q compile exec:java" ... This means that whenever we run a cdk deploy ... command, such that to synthesize a CloudFormation stack and deploy it, the maven-exec-plugin will be used to compile and package the code, before starting the associated main Java class. This is the most general case, the one of a classical Java CDK application. But to run a Quarkus application, we need to observe some special conditions. Quarkus packages an application as either a fast or a thin JAR and, if you aren't familiar with these terms, please don't hesitate to consult the documentation which explains them in detail. What interests us here is the fact that, by default, a fast JAR will be generated, under the name of quarkus-run.jar in the target/quarkus-app directory. Unless we're using Quarkus extensions for AWS, in which case a thin JAR is generated, in target/$finalName-runner.jar file, where $finalName is the value of the same element in pom.xml. In our case, we're using Quarkus extensions for AWS and, hence, a thin JAR will be created by the Maven build process. In order to run a Quarkus thin JAR, we need to manually modify the cdk.json file to replace the line above with the following one: JSON ... "app": "java -jar target/quarkus-app/quarkus-run.jar" ... The other important point to notice here is that, in general, a Quarkus application is exposing a REST API whose endpoint is started by the command above. But in our case, the one of a CDK application, there isn't any REST API and, hence, this endpoint needs to be specified in a different way. Look at our main class in the cdk-quarkus-api-gatewaymodule. Java @QuarkusMain public class CdkApiGatewayMain { public static void main(String... args) { Quarkus.run(CdkApiGatewayApp.class, args); } } Here, the @QuarkusMain annotation flags the subsequent class as the application's main endpoint and, further, using the io.quarkus.runtime.Quarkus.run() method will execute the mentioned class until it receives a signal like Ctrl-C, or one of the exit methods of the same API is called. So, we just saw how the CDK Quarkus application is started and that, once started, it runs the CdkApiGAtewayApp until it exits. This class is our CDK one which implements the App and that we've already seen in the previous post. But this time it looks differently, as you may see: Java @ApplicationScoped public class CdkApiGatewayApp implements QuarkusApplication { private CdkApiGatewayStack cdkApiGatewayStack; private App app; @Inject public CdkApiGatewayApp (App app, CdkApiGatewayStack cdkApiGatewayStack) { this.app = app; this.cdkApiGatewayStack = cdkApiGatewayStack; } @Override public int run(String... args) throws Exception { Tags.of(app).add("project", "API Gateway with Quarkus"); Tags.of(app).add("environment", "development"); Tags.of(app).add("application", "CdkApiGatewayApp"); cdkApiGatewayStack.initStack(); app.synth(); return 0; } } The first thing to notice is that this time, we're using the CDI (Context and Dependency Injection) implemented by Quarkus, also called ArC, which is a subset of the Jakarta CDI 4.1 specifications. It also has another particularity: it's a build-time CDI, as opposed to the runtime Jakarta EE one. The difference lies in the augmentation process, as explained previously. Another important point to observe is that the class implements the io.quarkus.runtime.QuarkusApplication interface which allows it to customize and perform specific actions in the context bootstrapped by the CdkApiGatewayMain class. As a matter of fact, it isn't recommended to perform such operations directly in the CdkApiGatewayMain since, at that point, Quarkus isn't completely bootstrapped and started yet. We need to define our class as @ApplicationScoped, such that to be instantiated only once. We also used constructor injection and took advantage of the producer pattern, as you may see in the CdkApiGatewayProducer class. We override the io.quarkus.runtime.QuarkusApplication.run() method such that to customize our App object by tagging it, as we already did in the previous example, and to invoke CdkApiGatewayStack, responsible to instantiate and initialize our CloudFormation stack. Last but not least, the app.synth() statement is synthesizing this stack and, once executed, our infrastructure, as defined by the CdkApiGatewayStack, should be deployed on the AWS cloud. Here is now the CdkApiGatewayStack class: Java @Singleton public class CdkApiGatewayStack extends Stack { @Inject LambdaWithBucketConstructConfig config; @ConfigProperty(name = "cdk.lambda-with-bucket-construct-id", defaultValue = "LambdaWithBucketConstructId") String lambdaWithBucketConstructId; @Inject public CdkApiGatewayStack(final App scope, final @ConfigProperty(name = "cdk.stack-id", defaultValue = "QuarkusApiGatewayStack") String stackId, final StackProps props) { super(scope, stackId, props); } public void initStack() { String functionUrl = new LambdaWithBucketConstruct(this, lambdaWithBucketConstructId, config).getFunctionUrl(); CfnOutput.Builder.create(this, "FunctionURLOutput").value(functionUrl).build(); } } This class has changed as well, compared to its previous release. It's a singleton that uses the concept of construct, which was introduced formerly. As a matter of fact, instead of defining the stack structure here, in this class, as we did before, we do it by encapsulating the stack's elements together with their configuration in a construct that facilitates easily assembled cloud applications. In our project, this construct is a part of a separate module, named cdk-simple-construct, such that we could reuse it repeatedly and increase the application's modularity. Java public class LambdaWithBucketConstruct extends Construct { private FunctionUrl functionUrl; public LambdaWithBucketConstruct(final Construct scope, final String id, LambdaWithBucketConstructConfig config) { super(scope, id); Role role = Role.Builder.create(this, config.functionProps().id() + "-role") .assumedBy(new ServicePrincipal("lambda.amazonaws.com")).build(); role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AmazonS3FullAccess")); role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("CloudWatchFullAccess")); IFunction function = Function.Builder.create(this, config.functionProps().id()) .runtime(Runtime.JAVA_21) .role(role) .handler(config.functionProps().handler()) .memorySize(config.functionProps().ram()) .timeout(Duration.seconds(config.functionProps().timeout())) .functionName(config.functionProps().function()) .code(Code.fromAsset((String) this.getNode().tryGetContext("zip"))) .build(); functionUrl = function.addFunctionUrl(FunctionUrlOptions.builder().authType(FunctionUrlAuthType.NONE).build()); new Bucket(this, config.bucketProps().bucketId(), BucketProps.builder().bucketName(config.bucketProps().bucketName()).build()); } public String getFunctionUrl() { return functionUrl.getUrl(); } } This is our construct which encapsulates our stack elements: a Lambda function with its associated IAM role and an S3 bucket. As you can see, it extends the software.construct.Construct class and its constructor, in addition to the standard scopeand id, parameters take a configuration object named LambdaWithBucketConstructConfig which defines, among others, properties related to the Lambda function and the S3 bucket belonging to the stack. Please notice that the Lambda function needs the IAM-managed policy AmazonS3FullAccess in order to read, write, delete, etc. to/from the associated S3 bucket. And since for tracing purposes, we need to log messages to the CloudWatch service, the IAM-managed policy CloudWatchFullAccess is required as well. These two policies are associated with a role whose naming convention consists of appending the suffix -role to the Lambda function name. Once this role is created, it will be attached to the Lambda function. As for the Lambda function body, please notice how this is created from an asset dynamically extracted from the deployment context. We'll come back in a few moments with more details concerning this point. Last but not least, please notice how after the Lambda function is created, a URL is attached to it and cached such that it can be retrieved by the consumer. This way we completely decouple the infrastructure logic (i.e., the Lambda function itself) from the business logic; i.e., the Java code executed by the Lambda function, in our case, a REST API implemented as a Quarkus JAX-RS (RESTeasy) endpoint, acting as a proxy for the API Gateway exposed by AWS. Coming back to the CdkApiGatewayStack class, we can see how on behalf of the Quarkus CDI implementation, we inject the configuration object LambdaWithBucketConstructConfig declared externally, as well as how we use the Eclipse MicroProfile Configuration to define its ID. Once the LambdaWithBucketConstruct instantiated, the only thing left to do is to display the Lambda function URL such that we can call it with different consumers, whether JUnit integration tests, curl utility, or postman. We just have seen the whole mechanics which allows us to decouple the two fundamental CDK building blocks App and Stack. We also have seen how to abstract the Stack building block by making it an external module which, once compiled and built as a standalone artifact, can simply be injected wherever needed. Additionally, we have seen the code executed by the Lambda function in our stack can be plugged in as well by providing it as an asset, in the form of a ZIP file, for example, and stored in the CDK deployment context. This code is, too, an external module named quarkus-api and consists of a REST API having a couple of endpoints allowing us to get some information, like the host IP address or the S3 bucket's associated attributes. It's interesting to notice how Quarkus takes advantage of the Qute templates to render HTML pages. For example, the following endpoint displays the attributes of the S3 bucket that has been created as a part of the stack. Java ... @Inject Template s3Info; @Inject S3Client s3; ... @GET @Path("info/{bucketName}") @Produces(MediaType.TEXT_HTML) public TemplateInstance getBucketInfo(@PathParam("bucketName") String bucketName) { Bucket bucket = s3.listBuckets().buckets().stream().filter(b -> b.name().equals(bucketName)) .findFirst().orElseThrow(); TemplateInstance templateInstance = s3Info.data("bucketName", bucketName, "awsRegionName", s3.getBucketLocation(GetBucketLocationRequest.builder().bucket(bucketName).build()) .locationConstraintAsString(), "arn", String.format(S3_FMT, bucketName), "creationDate", LocalDateTime.ofInstant(bucket.creationDate(), ZoneId.systemDefault()), "versioning", s3.getBucketVersioning(GetBucketVersioningRequest.builder().bucket(bucketName).build())); return templateInstance.data("tags", s3.getBucketTagging(GetBucketTaggingRequest.builder().bucket(bucketName).build()).tagSet()); } This endpoint returns a TemplateInstance whose structure is defined in the file src/main/resources/templates/s3info.htmland which is filled with data retrieved by interrogating the S3 bucket in our stack, on behalf of the S3Client class provided by the AWS SDK. A couple of integration tests are provided and they take advantage of the Quarkus integration with AWS, thanks to which it is possible to run local cloud services, on behalf of testcontainers and localstack. In order to run them, proceed as follows: Shell $ git clone https://github.com/nicolasduminil/cdk $ cd cdk/cdk-quarkus/quarkus-api $ mvn verify Running the sequence of commands above will produce a quite verbose output and, at the end, you'll see something like this: Shell [INFO] [INFO] Results: [INFO] [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] [INFO] --- failsafe:3.2.5:verify (default) @ quarkus-api --- [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 22.344 s [INFO] Finished at: 2024-07-04T17:18:47+02:00 [INFO] ------------------------------------------------------------------------ That's not a big deal - just a couple of integration tests executed against a localstack running in testcontainers to make sure that everything works as expected. But if you want to test against real AWS services, meaning that you fulfill the requirements, then you should proceed as follows: Shell $ git clone https://github.com/nicolasduminil/cdk $ cd cdk $ ./deploy.sh cdk-quarkus/cdk-quarkus-api-gateway cdk-quarkus/quarkus-api/ Running the script deploy.sh with the parameters shown above will synthesize and deploy your stack. These two parameters are: The CDK application module name: This is the name of the Maven module where your cdk.json file is. The REST API module name: This is the name of the Maven module where the function.zip file is. If you look at the deploy.sh file, you'll see the following: Shell ...cdk deploy --all --context zip=~/cdk/$API_MODULE_NAME/target/function.zip... This command deploys the CDK app after having set in the zip context variable the function.zip location. Do you remember that the Lambda function has been created in the stack (LambdaWithBucketConstruct class) like this? Java IFunction function = Function.Builder.create(this, config.functionProps().id()) ... .code(Code.fromAsset((String) this.getNode().tryGetContext("zip"))) .build(); The statement below gets the asset stored in the deployment context under the context variable zip and uses it as the code that will be executed by the Lambda function. The output of the deploy.sh file execution (quite verbose as well) will finish by displaying the Lambda function URL: Shell ... Outputs: QuarkusApiGatewayStack.FunctionURLOutput = https://...lambda-url.eu-west-3.on.aws/ Stack ARN: arn:aws:cloudformation:eu-west-3:...:stack/QuarkusApiGatewayStack/... ... Now, in order to test your stack, you may fire your preferred browser at https://<generated>.lambda-url.eu-west-3.on.aws/s3/info/my-bucket-8701 and should see something looking like this: Conclusion Your test is successful and you now know how to use CDK constructs to create infrastructure standalone modules and assemble them into AWS CloudFormation stacks. But there is more, so stay tuned!
Creating a solid REST API in Java requires more than a basic grasp of HTTP requests and responses. Ensuring that your API is well-designed, maintainable, and secure is essential. This article will offer four critical tips to improve your REST API. It assumes you are already acquainted with the Richardson Maturity Model, especially up to Level 2, which is the minimum requirement for a good API. If you need a quick reminder about the Richardson Maturity Model, I recommend reading this article by Martin Fowler: Richardson Maturity Model. Given that prerequisite, let’s dive into the tips. For illustration purposes, we’ll use an example from the expedition domain. While we won’t focus on entities and layers in detail, imagine we have the following entity class: Java public class Expedition { private String name; private String location; private LocalDate date; public Expedition(String name, String location, LocalDate date) { this.name = name; this.location = location; this.date = date; } public String getName() { return name; } public String getLocation() { return location; } public LocalDate getDate() { return date; } } 1. Consistency in Terminology and Resource Naming One of the most important aspects of designing a good REST API is ensuring consistent terminology and careful attention to the service’s vocabulary. Start with a generic naming convention and then move to more specific terms. Following Domain-Driven Design (DDD) principles, begin with the main domain and then refine it into subdomains. A simple rule of thumb is to use plural nouns for resources. For example: GET /expeditions - Returns all expeditions GET /expeditions/{id} - Retrieves a specific expedition by its ID Sample code: Java @Path("expeditions") public class ExpeditionResource { @GET public List<Expedition> list() { // implementation here } @GET @Path("/{id}") public Expedition get(@PathParam("id") String id) { // implementation here } @GET @Path("/search") public List<Expedition> mine() { // implementation here } } For more detailed guidelines on maintaining consistency, refer to the REST API Design Rulebook. 2. Maintainability, Scalability, and Documentation Maintaining and scaling your API is crucial as it grows in complexity. One way to ensure maintainability is through proper documentation. While documentation may not be the favorite task of many developers, it’s indispensable. OpenAPI is an excellent tool for generating and enhancing documentation automatically. For more, visit OpenAPI. Another critical aspect is versioning. Versioning ensures backward compatibility and smooth transitions between different API versions. It allows you to support both old and new versions simultaneously, encouraging users to migrate to the latest version at their convenience. You can achieve this in Java by structuring your code with separate packages for each version and creating adapter layers to manage interactions between versions. Example: Java package os.expert.demo.expeditions.v1; @Path(”/api/v1/expeditions") public class ExpeditionResource { // implementation here } package os.expert.demo.expeditions.v2; @Path("/api/v2/expeditions") public class ExpeditionResource { // implementation here 3. Security: Never Trust the User Security is a fundamental aspect of any API. A general rule is to never trust the user; always validate their permission to access the requested resources. One practical approach is to use authentication to determine which expeditions a user can access without relying on user-provided IDs. Example: Java @GET @Path("/my-expeditions") public List<Expedition> myExpeditions() { // No need to request IDs since the user is authenticated // implementation here } This principle should also apply to other operations like editing or deleting resources. Always validate permissions before proceeding. 4. Exception Handling and Proper HTTP Status Codes Finally, a well-designed API should have robust exception handling that maps errors to the correct HTTP status codes. For instance, if an expedition is not found, your API should return a 404 Not Found status code, maintaining consistency between your Java code and REST API semantics. Java @Provider public class ExpeditionNotFoundExceptionMapper implements ExceptionMapper<ExpeditionNotFoundException> { @Override public Response toResponse(ExpeditionNotFoundException exception) { return Response.status(Response.Status.NOT_FOUND).entity(exception.getMessage()).build(); } } Conclusion In summary, creating a solid REST API involves several key steps: Understand the basics - Start familiarizing yourself with the Richardson Maturity Model. Use consistent terminology - Follow a clear and consistent resource naming convention. Focus on maintainability and documentation - Implement versioning and generate documentation using tools like OpenAPI. Prioritize security - Always validate user permissions. Implement proper exception handling - Ensure your API returns appropriate HTTP status codes. By following these tips, you’ll be well on your way to developing a reliable and maintainable REST API in Java, a challenge even experienced developers can struggle with. For sample code and further examples, visit the repository on GitHub. Video
Previously, we examined the happens before guarantee in Java. This guarantee gives us confidence when we write multithreaded programs with regard to the re-ordering of statements that can happen. In this post, we shall focus on variable visibility between two threads and what happens when we change a variable that is shared. Code Examination Let’s examine the following code snippet: Java import java.util.Date; public class UnSynchronizedCountDown { private int number = Integer.MAX_VALUE; public Thread countDownUntilAsync(final int threshold) { return new Thread(() -> { while (number>threshold) { number--; System.out.println("Decreased "+number +" at "+ new Date()); } }); } private void waitUntilThresholdReached(int threshold) { while (number>threshold) { } } public static void main(String[] args) { int threshold = 2125840327; UnSynchronizedCountDown unSynchronizedCountDown = new UnSynchronizedCountDown(); unSynchronizedCountDown.countDownUntilAsync(threshold).start(); unSynchronizedCountDown.waitUntilThresholdReached(threshold); System.out.println("Threshold reached at "+new Date()); } } This is a bad piece of code: two threads operate on the same variable number without any synchronization. Now the code will likely run forever! Regardless of when the countDown thread reaches the goal, the main thread will not pick the new value which is below the threshold. This is because the changes made to the number variable have not been made visible to the main thread. So it’s not only about synchronizing and issuing thread-safe operations but also ensuring that the changes a thread has made are visible. Visibility and Synchronized Intrinsic locking in Java guarantees that one thread can see the changes of another thread. So when we use synchronized the changes of a thread become visible to the other thread that has stumbled on the synchronized block. Let’s change our example and showcase this: Java package com.gkatzioura.concurrency.visibility; public class SynchronizedCountDown { private int number = Integer.MAX_VALUE; private String message = "Nothing changed"; private static final Object lock = new Object(); private int getNumber() { synchronized (lock) { return number; } } public Thread countDownUntilAsync(final int threshold) { return new Thread(() -> { message = "Count down until "+threshold; while (number>threshold) { synchronized (lock) { number--; if(number<=threshold) { } } } }); } private void waitUntilThresholdReached(int threshold) { while (getNumber()>threshold) { } } public static void main(String[] args) { int threshold = 2147270516; SynchronizedCountDown synchronizedCountDown = new SynchronizedCountDown(); synchronizedCountDown.countDownUntilAsync(threshold).start(); System.out.println(synchronizedCountDown.message); synchronizedCountDown.waitUntilThresholdReached(threshold); System.out.println(synchronizedCountDown.message); } } Access to the number variable is protected by a lock. Also modifying the variable is synchronized using the same lock. Eventually, the program will terminate as expected since we will reach the threshold. Every time we enter the synchronized block the changes made by the countdown thread will be visible to the main thread. This applies not only to the variables involved on a synchronized block but also to the variables that were visible to the countdown thread. Thus although the message variable was not inside any synchronized block at the end of the program its altered value got publicized, thus saw the right value printed.
Imagine inheriting a codebase where classes are clean and concise, and developers don't have to worry about boilerplate code because they can get automatically generated getters, setters, constructors, and even builder patterns. Meet Lombok, a library used for accelerating development through "cleaning" boilerplate code and injecting it automatically during compile time. But, is Lombok a hero or a villain in disguise? Let's explore the widespread perceived benefits and potential drawbacks of its adoption in enterprise Java solutions. Overview Enterprise software is designed as a stable and predictable solution. When adopting Lombok, a framework that modifies code at compile time, we are navigating towards the opposite direction, through seas of unpredictable results and hidden complexities. It's a choice that may put an enterprise application's long-term success at risk. Architects have the tough responsibility of making decisions that will reflect throughout a software's life cycle - from development to sustaining. During development phases, when considering ways to improve developer productivity, it's crucial to balance the long-term impacts of each decision on the code complexity, predictability, and maintainability, while also considering that software will rely on multiple frameworks that must be able to correctly function with each other without incompatibilities that directly interfere on each other's behaviors. Let's have a close look at different ways that Lombok is used, the common thoughts around it, and explore associated trade-offs. In-Depth Look Let's explore practical use cases and some developer's statements I've heard over the years, while we explore the aspects around the ideas. “Lombok Creates Getters and Constructors, Saving Me Time on Data Classes” Nowadays, we can use powerful IDEs and their code-generation features to create our getters, setters, and builders. It's best to use it to zealously and consciously generate code. Lombok's annotations can lead to unexpected mutability: @Data, by default, generates public setters, which violates encapsulation principles. Lombok offers ways to mitigate this through the usage of annotations such as @Value and @Getters(AccessLevel.NONE), although this is an error-prone approach, as now your code is "vulnerable by default," and it's up to you to remember to adjust this every time. Given the fact that code generation to some degree reduces the thought processes during the implementation, these configurations can be overseen by developers who might happen to forget, or maybe who don't know enough about Lombok to be aware of this need. @Builder generates a mutable builder class, which can lead to inconsistent object states. Remember the quote from Joshua Bloch in his book, Effective Java: "Classes should be immutable unless there's a very good reason to make them mutable." See an example of an immutable class, which is not an anemic model: Java public final class Customer { //final class private final String id; private final String name; private final List<Order> orders; private Customer(String id, String name, List<Order> orders) { this.id = Objects.requireNonNull(id); this.name = Objects.requireNonNull(name); this.orders = List.copyOf(orders); // Defensive copy } public static Customer create(String id, String name, List<Order> orders) { return new Customer(id, name, orders); } // Getters (no setters,for immutability) public String getId() { return id; } public String getName() { return name; } public List<Order> getOrders() { return List.copyOf(orders); } // Explicit methods for better control @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; Customer customer = (Customer) o; return id.equals(customer.id); } @Override public int hashCode() { return Objects.hash(id); } } “Utility Classes and Exceptions Are a Breeze With Lombok” Developers may often use Lombok to accelerate exception class creation: Java @Getter public class MyAppGenericException extends RuntimeException { private final String error; private final String message; } While this approach reduces boilerplate code, you may end up with overly generic exceptions and add difficulties for those wanting to create proper exception handling. A suggestion for a better approach is to create specific exception classes with meaningful constructors. In this case, it's essential to keep in mind that, as discussed before, hidden code leads to reduced clarity and creates uncertainty on how exceptions should be used and extended properly. In the example, if the service was designed to use the MyAppGenericException as the main parent exception, developers would now rely on a base class that can be confusing since all constructors and methods are hidden. This particular characteristic may result in worse productivity in larger teams, as the level of understanding of Lombok will differ across developers, not to mention the increased complexity of new developers or code maintainers to understand how everything fits together. For the reasons presented so far, Lombok's @UtilityClass can also be misleading: Java @UtilityClass public class ParseUtils { public static CustomerId parseCustomerId(String CustomerIdentifier) { //... } } Instead, a standard-based approach is recommended: Java public final class ParseUtils { private ParseUtils() { throw new AssertionError("This class should not be instantiated."); } public static CustomerId parseCustomerId(String CustomerIdentifier) { //... } } "Logging Is Effortless With @Slf4j in My Classes" Another usage of the auto-generation capabilities of Lombok is for boilerplate logging setup within classes through the @Slf4j annotation : Java @Slf4j public class MyService { public void doSomething() { log.info("log when i'm doing something"); } } You have just tightly coupled the implementation of logging capabilities using a particular framework (Slf4j) with your code implementation. Instead, consider using CDI for a more flexible approach: Java public class SomeService { private final Logger logger; public SomeService(Logger logger) { this.logger = Objects.requireNonNull(logger); } public void doSomething() { logger.info("Doing something"); } } “Controlling Access and Updating an Attribute To Reflect a DB Column Change, for Example, Is Way Simpler With Lombok” Developers argue that addressing some types of changes in the code can be way faster when not having boilerplate code. For example, in Hibernate entities, changes in database columns could reflect updating the code of attributes and getters/setters. Instead of tightly coupling the database and code implementation (e.g., attribute name and column name), consider alternatives that provide proper abstraction between these two layers, such as the Hibernate annotations for customizing column names. Finally, you may also want better control over the persistence behaviors instead of hidden generated code. Another popular annotation in Lombok is @With. It's used to create a deep copy of an object and may result in excessive object creation, without any validation of business rules. “@Builder Simplifies Creating and Working With Complex Objects” Oversimplified domain models and anemic models are expected results for projects that rely on Lombok. On the generation of the equals, hashcode and toString methods, be aware of the following: @EqualsAndHashCode may conflict with entity identity in JPA, resulting in unexpected behaviors on the comparison between detached entities, or collections' operations. Java @Entity @EqualsAndHashCode public class Order { @Id @GeneratedValue private Long id; private String orderNumber; // Other fields... } @Data automatically creates toString() methods that by default expose all attributes, including, sensitive information. Consider carefully implementing these methods based on domain requirements: Java @Entity public class User { @Id private Long id; private String username; private String passwordHash; // Sensitive information @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User)) return false; User user = (User) o; return Objects.equals(id, user.id); } @Override public int hashCode() { return Objects.hash(id); } @Override public String toString() { return "User{id=" + id + ", username='" + username + "'}"; // Note: passwordHash is deliberately excluded } } “Lombok Lets Me Use Inheritance, Unlike Java Records” It's true that when using records, we can't use hierarchy. However, this is a limitation that often has us delivering better code design. Here's how to address this need through the use of composition and interfaces: Java public interface Vehicle { String getRegistrationNumber(); int getNumberOfWheels(); } public record Car(String registrationNumber, String model) implements Vehicle { @Override public int getNumberOfWheels() { return 4; } } public record Motorcycle(String registrationNumber, boolean hasSidecar) implements Vehicle { @Override public int getNumberOfWheels() { return hasSidecar ? 3 : 2; } } “Lombok Streamlines My Build Process and Maintenance” Lombok's magic comes at the cost of code clarity and goes against SOLID principles: Hidden implementation: You can not see the generated methods in the source code. Developers may face challenges to fully understanding all the class' behaviors without dedicating time to learn how Lombok works behind the scenes. Debugging complications: Debugging the code may not work consistently as the source code you have often is not a reflection of the behavior in runtime. Final Thoughts "The ratio of time spent reading versus writing is well over 10 to 1... Making it easy to read makes it easier to write." - Robert C. Martin, Clean Code While Lombok offers short-term productivity gains, its use in enterprise Java development introduces significant risks to code maintainability, readability, and long-term project health. To avoid the challenges we've explored that derive from Lombok usage, consider alternative options that give you much higher chances of creating more stable, maintainable, and predictable code. Developers who seek to deliver successful, long-term, enterprise software projects in critical domains have higher chances to succeed in their endeavors by embracing best practices and good principles of Java development for creating robust, maintainable, and secure software. Learn More "Unraveling Lombok’s Code Design Pitfalls: Exploring the Pros and Cons," Otavio Santana
In today’s rapidly evolving enterprise landscape, managing and synchronizing data across complex environments is a significant challenge. As businesses increasingly adopt multi-cloud strategies to enhance resilience and avoid vendor lock-in, they are also turning to edge computing to process data closer to the source. This combination of multi-cloud and edge computing offers significant advantages, but it also presents unique challenges, particularly in ensuring seamless and reliable data synchronization across diverse environments. In this post, we’ll explore how the open-source KubeMQ’s Java SDK provides an ideal solution for these challenges. We’ll focus on a real-life use case involving a global retail chain that uses KubeMQ to manage inventory data across its multi-cloud and edge infrastructure. Through this example, we’ll demonstrate how the solution enables enterprises to achieve reliable, high-performance data synchronization, transforming their operations. The Complexity of Multi-Cloud and Edge Environments Enterprises today are increasingly turning to multi-cloud architectures to optimize costs, enhance system resilience, and avoid being locked into a single cloud provider. However, managing data across multiple cloud providers is far from straightforward. The challenge is compounded when edge computing enters the equation. Edge computing involves processing data closer to where it’s generated, such as in IoT devices or remote locations, reducing latency and improving real-time decision-making. When multi-cloud and edge computing are combined, the result is a highly complex environment where data needs to be synchronized not just across different clouds but also between central systems and edge devices. Achieving this requires a robust messaging infrastructure capable of managing these complexities while ensuring data consistency, reliability, and performance. KubeMQ’s Open-Source Java SDK: A Unified Solution for Messaging Across Complex Environments KubeMQ is a messaging and queue management solution designed to handle modern enterprise infrastructure. The KubeMQ Java SDK is particularly appropriate for developers working within Java environments, offering a versatile toolset for managing messaging across multi-cloud and edge environments. Key features of the KubeMQ Java SDK include: All messaging patterns in one SDK: KubeMQ’s Java SDK supports all major messaging patterns, providing developers with a unified experience that simplifies integration and development. Utilizes GRPC streaming for high performance: The SDK leverages GRPC streaming to deliver high performance, making it suitable for handling large-scale, real-time data synchronization tasks. Simplicity and ease of use: With numerous code examples and encapsulated logic, the SDK simplifies the development process by managing complexities typically handled on the client side. Real-Life Use Case: Retail Inventory Management Across Multi-Cloud and Edge To illustrate how to use KubeMQ’s Java SDK, let’s consider a real-life scenario involving a global retail chain. This retailer operates thousands of stores worldwide, each equipped with IoT devices that monitor inventory levels in real-time. The company has adopted a multi-cloud strategy to enhance resilience and avoid vendor lock-in while leveraging edge computing to process data locally at each store. The Challenge The retailer needs to synchronize inventory data from thousands of edge devices across different cloud providers. Ensuring that every store has accurate, up-to-date stock information is critical for optimizing the supply chain and preventing stockouts or overstock situations. This requires a robust, high-performance messaging system that can handle the complexities of multi-cloud and edge environments. The Solution Using the KubeMQ Java SDK, the retailer implements a messaging system that synchronizes inventory data across its multi-cloud and edge infrastructure. Here’s how the solution is built: Store Side Code Step 1: Install KubeMQ SDK Add the following dependency to your Maven pom.xml file: XML <dependency> <groupId>io.kubemq.sdk</groupId> <artifactId>kubemq-sdk-Java</artifactId> <version>2.0.0</version> </dependency> Step 2: Synchronizing Inventory Data Across Multi-Clouds Java import io.kubemq.sdk.queues.QueueMessage; import io.kubemq.sdk.queues.QueueSendResult; import io.kubemq.sdk.queues.QueuesClient; import java.util.UUID; public class StoreInventoryManager { private final QueuesClient client1; private final QueuesClient client2; private final String queueName = "store-1"; public StoreInventoryManager() { this.client1 = QueuesClient.builder() .address("cloudinventory1:50000") .clientId("store-1") .build(); this.client2 = QueuesClient.builder() .address("cloudinventory2:50000") .clientId("store-1") .build(); } public void sendInventoryData(String inventoryData) { QueueMessage message = QueueMessage.builder() .channel(queueName) .body(inventoryData.getBytes()) .metadata("Inventory Update") .id(UUID.randomUUID().toString()) .build(); try { // Send to cloudinventory1 QueueSendResult result1 = client1.sendQueuesMessage(message); System.out.println("Sent to cloudinventory1: " + result1.isError()); // Send to cloudinventory2 QueueSendResult result2 = client2.sendQueuesMessage(message); System.out.println("Sent to cloudinventory2: " + result2.isError()); } catch (RuntimeException e) { System.err.println("Failed to send inventory data: " + e.getMessage()); } } public static void main(String[] args) { StoreInventoryManager manager = new StoreInventoryManager(); manager.sendInventoryData("{'item': 'Laptop', 'quantity': 50}"); } } Cloud Side Code Step 1: Install KubeMQ SDK Add the following dependency to your Maven pom.xml file: XML <dependency> <groupId>io.kubemq.sdk</groupId> <artifactId>kubemq-sdk-Java</artifactId> <version>2.0.0</version> </dependency> Step 2: Managing Data on Cloud Side Java import io.kubemq.sdk.queues.QueueMessage; import io.kubemq.sdk.queues.QueuesPollRequest; import io.kubemq.sdk.queues.QueuesPollResponse; import io.kubemq.sdk.queues.QueuesClient; public class CloudInventoryManager { private final QueuesClient client; private final String queueName = "store-1"; public CloudInventoryManager() { this.client = QueuesClient.builder() .address("cloudinventory1:50000") .clientId("cloudinventory1") .build(); } public void receiveInventoryData() { QueuesPollRequest pollRequest = QueuesPollRequest.builder() .channel(queueName) .pollMaxMessages(1) .pollWaitTimeoutInSeconds(10) .build(); try { while (true) { QueuesPollResponse response = client.receiveQueuesMessages(pollRequest); if (!response.isError()) { for (QueueMessage msg : response.getMessages()) { String inventoryData = new String(msg.getBody()); System.out.println("Received inventory data: " + inventoryData); // Process the data here // Acknowledge the message msg.ack(); } } else { System.out.println("Error receiving messages: " + response.getError()); } // Wait for a bit before polling again Thread.sleep(1000); } } catch (RuntimeException | InterruptedException e) { System.err.println("Failed to receive inventory data: " + e.getMessage()); } } public static void main(String[] args) { CloudInventoryManager manager = new CloudInventoryManager(); manager.receiveInventoryData(); } } The Benefits of Using KubeMQ for Retail Inventory Management Implementing KubeMQ’s Java SDK in this retail scenario offers several benefits: Improved inventory accuracy: The retailer can ensure that all stores have accurate, up-to-date stock information, reducing the risk of stockouts and overstock. Optimized supply chain: Accurate data flow from the edge to the cloud streamlines the supply chain, reducing waste and improving response times. Enhanced resilience: The multi-cloud and edge approach provides a resilient infrastructure that can adapt to regional disruptions or cloud provider issues. Conclusion KubeMQ’s open-source Java SDK provides a powerful solution for enterprises looking to manage data across complex multi-cloud and edge environments. In the retail use case discussed, the SDK enables seamless data synchronization, transforming how the retailer manages its inventory across thousands of stores worldwide. For more information and support, check out their quick start, documentation, tutorials, and community forums. Have a really great day!
One of the first decisions you’ll need to make when working with the AWS Cloud Development Kit (CDK) is choosing the language for writing your Infrastructure as Code (IaC). The CDK currently supports TypeScript, JavaScript, Python, Java, C#, and Go. Over the past few years, I’ve worked with the CDK in TypeScript, Python, and Java. While there is ample information available online for TypeScript and Python, this post aims to share my experience using Java as the language of choice for the AWS CDK. Wait…What? Use Java With the AWS CDK? Some may say that TypeScript is the most obvious language to use while working with the AWS CDK. The CDK itself is written in TypeScript and it’s also the most used language according to the 2023 CDK Community Survey. Java is coming in 3rd place with a small percentage of use. I do wonder if this still holds true given the number of responses to the survey. I’ve worked with small businesses and large enterprise organizations over the last years and I see more and more Java-oriented teams move their workloads to AWS while adopting AWS CDK as their Infrastructure as Code tool. Depending on the type of service(s) being built by these teams they may or may not have any experience with either Python or TypeScript and the Node.js ecosystem, which makes sticking to Java an easy choice. General Observations From what I’ve seen, adopting the CDK in Java is relatively easy for most of these teams as they already understand the language and the ecosystem. Integrating the CDK with their existing build tools like Maven and Gradle is well documented, which leaves them with the learning curve of understanding how to work with infrastructure as code, how to structure a CDK project, and when to use L1, L2, and L3 constructs. Compared to TypeScript the CDK stacks and constructs written in Java contain a bit more boilerplate code and therefore might feel a bit more bloated if you come from a different language. I personally don’t feel this makes the code less readable and with modern IDEs and coding assistants, I don’t feel I’m less productive. The CDK also seems to become more widely adopted in the Java community with more recent Java frameworks like Micronaut even having built-in support for AWS CDK in the framework. See for instance the following Micronaut launch configurations: Micronaut Application with API Gateway and CDK for Java runtime Micronaut Function with API Gateway and CDK for Java runtime One of the advantages of Java is that it’s a statically typed language, which means it will catch most CDK coding errors during compile time. There are still some errors which you will only see during an actual cdk synth or cdk deploy. For instance, some constructs have required properties that will only become visible if you try to synthesize the stack, but in my experience, you will have that in other languages as well. Performance-wise, it feels like the CDK in Java is a bit slower compared to using TypeScript or any other interpreted language. I’ve not measured this, but it’s more of a gut feeling. This might have to do with the static nature of Java and its corresponding build tools and compile phase. On the other hand, it might be that the JSII runtime architecture also has an effect and how Java interacts with a JavaScript environment. Java Builders One of the biggest differences when using the AWS CDK with Java is the use of Builders. When creating constructs with TypeScript, you’re mainly using the props argument (map of configuration properties) while creating a construct. Let’s take a look at an example: TypeScript const bucket = new s3.Bucket(this,"MyBucket", { versioned: true, encryption: BucketEncryption.KMS_MANAGED }) The Java version of the above snippet uses a Builder class that follows the builder pattern for constructing the properties. If you’re unfamiliar with the Builder pattern in Java, I recommend checking out this blog post about using the Builder pattern. Depending on the CDK construct, you might be able to define a CDK resource in two different ways. In the first example, you use the Builder for the Bucket properties. Java Bucket bucket = new Bucket(this, "MyBucket", new BucketProps.Builder() .versioned(true) .encryption(BucketEncryption.KMS_MANAGED) .build()); The alternative is that constructs can have their own builder class, which makes it a little less verbose and easier to read. Java Bucket bucket = Bucket.Builder .create(this, "MyBucket") .versioned(true) .encryption(BucketEncryption.KMS_MANAGED) .build(); IDE Support Overall IDE support is really great when working with CDK in Java. I use IntelliJ IDEA on a daily basis and auto-completion really helps when using the Builder objects. As the CDK documentation is also inside the CDK Java source code, looking up documentation is really easy. It’s similar to how you would do it with any kind of other object or library. Third-Party Construct Support The CDK itself is written in TypeScript, and for each supported programming language, a specific binding is generated. This means that when a new resource or feature for an AWS service is added in the TypeScript variant of the CDK, it’s also available to developers using a Java-based CDK. Besides the default CDK constructs, there are also a lot of community-generated constructs. Construct Hub is a great place to find them. From what I’ve seen, most constructs coming out of AWS will support Java as one of the default languages. Community-supported constructs however might not. There are several popular constructs that only support TypeScript and Python. Filtering on Construct Hub for AWS CDK v2-based constructs, sorted by programming languages results in the following data. Language Number of constructs libraries Typescript 1164 Python 781 .Net 511 Java 455 Go 132 Depending on the type of infrastructure or third-party services you’re planning to use, you might not be able to use all available constructs. For instance, the constructs maintained by DataDog are only available in Typescript, Python, and Go. In my personal experience, though, most construct developers are open to supporting Java. Third-party constructs are based on projen and jsii, which means that adding a Java-based version is most of the time a matter of configuration in the package.json file of the project. JSON "jsii": { "outdir": "dist", "targets": { "java": { "package": "io.github.cdklabs.cdknag", "maven": { "groupId": "io.github.cdklabs", "artifactId": "cdknag" } }, "python": { "distName": "cdk-nag", "module": "cdk_nag" }, "dotnet": { "namespace": "Cdklabs.CdkNag", "packageId": "Cdklabs.CdkNag" }, "go": { "moduleName": "github.com/cdklabs/cdk-nag-go" } }, "tsc": { "outDir": "lib", "rootDir": "src" } }, (An example of how JSII is configured for the CDK NAG project) Once the configuration is in place and the artifacts have been pushed to, for instance, Maven Central, you’re good to go. When thinking about it, I once had a 3rd party construct I wanted to use that did not support Java (yet). It got added quite quickly and there was also an alternative solution for it, so I can't remember having issues with the lower number of available constructs. Examples, Tutorials, and Documentation I think it’s good to reflect on the fact that there are more CDK examples and tutorials available in TypeScript and Python compared to Java. This reflects the findings in the usage chart from the CDK Community Survey. However, reading TypeScript as a Java programmer is relatively easy (in my personal opinion). If you’re new to the AWS CDK, there is a ton of example code available on GitHub, YouTube, and numerous blog posts and tutorials. If you’re already using the CDK in combination with Java be sure to write some blog posts or tutorials, so others can see that and benefit from your knowledge! Summary Java is a very viable option when working with the AWS CDK, especially for workload teams already familiar with the language and its ecosystem. IDE support for the CDK is excellent with features like auto-completion and easy access to source code documentation. All in all, the experience is really good. Keep in mind that picking Java for your infrastructure as code all depends on the context and the environment you’re in. I would suggest picking the language that is most applicable to your specific situation. If you still need to make the choice and are already working with Java, I would definitely recommend trying it out!
Garbage Collection (GC) plays an important role in Java’s memory management. It helps to reclaim memory that is no longer in use. A garbage collector uses its own set of threads to reclaim memory. These threads are called GC threads. Sometimes JVM can end up either with too many or too few GC threads. In this post, we will discuss why JVM can end up having too many/too few GC threads, the consequences of it, and potential solutions to address them. How To Find Your Application’s GC Thread Count You can determine your application’s GC thread count by doing a thread dump analysis as outlined below: Capture thread dump from your production server. Analyze the dump using a thread dump analysis tool. The tool will immediately report the GC thread count, as shown in the figure below. Figure 1: fastThread tool reporting GC Thread count How To Set GC Thread Count You can manually adjust the number of GC threads by setting the following two JVM arguments: -XX:ParallelGCThreads=n: Sets the number of threads used in the parallel phase of the garbage collectors -XX:ConcGCThreads=n: Controls the number of threads used in concurrent phases of garbage collectors What Is the Default GC Thread Count? If you don’t explicitly set the GC thread count using the above two JVM arguments, then the default GC thread count is derived based on the number of CPUs in the server/container. –XX:ParallelGCThreads Default: On Linux/x86 machines, it is derived based on the formula: if (num of processors <=8) { return num of processors; } else { return 8+(num of processors-8)*(5/8); } So if your JVM is running on a server with 32 processors, then the ParallelGCThread value is going to be: 23(i.e. 8 + (32 – 8)*(5/8)). -XX:ConcGCThreads Default: It’s derived based on the formula: max((ParallelGCThreads+2)/4, 1) So if your JVM is running on a server with 32 processors, then: ParallelGCThread value is going to be: 23 (i.e. 8 + (32 – 8)*(5/8)). ConcGCThreads value is going to be: 6 (i.e. max(25/4, 1). Can JVM End Up With Too Many GC Threads? It’s possible for your JVM to unintentionally have too many GC threads, often without your awareness. This typically happens because the default number of GC threads is automatically determined based on the number of CPUs in your server or container. For example, on a machine with 128 CPUs, the JVM might allocate around 80 threads for the parallel phase of garbage collection and about 20 threads for the concurrent phase, resulting in a total of approximately 100 GC threads. If you’re running multiple JVMs on this 128-CPU machine, each JVM could end up with around 100 GC threads. This can lead to excessive resource usage because all these threads are competing for the same CPU resources. This problem is particularly noticeable in containerized environments, where multiple applications share the same CPU cores. It will cause JVM to allocate more GC threads than necessary, which can degrade overall performance. Why Is Having Too Many GC Threads a Problem? While GC threads are essential for efficient memory management, having too many of them can lead to significant performance challenges in your Java application. Increased context switching: When the number of GC threads is too high, the operating system must frequently switch between these threads. This leads to increased context switching overhead, where more CPU cycles are spent managing threads rather than executing your application’s code. As a result, your application may slow down significantly. CPU overhead: Each GC thread consumes CPU resources. If too many threads are active simultaneously, they can compete for CPU time, leaving less processing power available for your application’s primary tasks. This competition can degrade your application’s performance, especially in environments with limited CPU resources. Memory contention: With an excessive number of GC threads, there can be increased contention for memory resources. Multiple threads trying to access and modify memory simultaneously can lead to lock contention, which further slows down your application and can cause performance bottlenecks. Increased GC pause times and lower throughput: When too many GC threads are active, the garbage collection process can become less efficient, leading to longer GC pause times where the application is temporarily halted. These extended pauses can cause noticeable delays or stutters in your application. Additionally, as more time is spent on garbage collection rather than processing requests, your application’s overall throughput may decrease, handling fewer transactions or requests per second and affecting its ability to scale and perform under load. Higher latency: Increased GC activity due to an excessive number of threads can lead to higher latency in responding to user requests or processing tasks. This is particularly problematic for applications that require low latency, such as real-time systems or high-frequency trading platforms, where even slight delays can have significant consequences. Diminishing returns: Beyond a certain point, adding more GC threads does not improve performance. Instead, it leads to diminishing returns, where the overhead of managing these threads outweighs the benefits of faster garbage collection. This can result in degraded application performance, rather than the intended optimization. Why Is Having Too Few GC Threads a Problem? While having too many GC threads can create performance issues, having too few GC threads can be equally problematic for your Java application. Here’s why: Longer Garbage Collection times: With fewer GC threads, the garbage collection process may take significantly longer to complete. Since fewer threads are available to handle the workload, the time required to reclaim memory increases, leading to extended GC pause times. Increased application latency: Longer garbage collection times result in increased latency, particularly for applications that require low-latency operations. Users might experience delays, as the application becomes unresponsive while waiting for garbage collection to finish. Reduced throughput: A lower number of GC threads means the garbage collector can’t work as efficiently, leading to reduced overall throughput. Your application may process fewer requests or transactions per second, affecting its ability to scale under load. Inefficient CPU utilization: With too few GC threads, the CPU cores may not be fully utilized during garbage collection. This can lead to inefficient use of available resources, as some cores remain idle while others are overburdened. Increased risk of OutOfMemoryErrors and memory leaks: If the garbage collector is unable to keep up with the rate of memory allocation due to too few threads, it may not be able to reclaim memory quickly enough. This increases the risk of your application running out of memory, resulting in OutOfMemoryErrors and potential crashes. Additionally, insufficient GC threads can exacerbate memory leaks by slowing down the garbage collection process, allowing more unused objects to accumulate in memory. Over time, this can lead to excessive memory usage and further degrade application performance. Solutions To Optimize GC Thread Count If your application is suffering from performance issues due to an excessive or insufficient number of GC threads, consider manually setting the GC thread count using the above-mentioned JVM arguments: -XX:ParallelGCThreads=n -XX:ConcGCThreads=n Before making these changes in production, it’s essential to study your application’s GC behavior. Start by collecting and analyzing GC logs using tools. This analysis will help you identify if the current thread count is causing performance bottlenecks. Based on these insights, you can make informed adjustments to the GC thread count without introducing new issues Note: Always test changes in a controlled environment first to confirm that they improve performance before rolling them out to production. Conclusion Balancing the number of GC threads is key to ensuring your Java application runs smoothly. By carefully monitoring and adjusting these settings, you can avoid potential performance issues and keep your application operating efficiently.
Ever notice that custom OOP projects tend towards a flaming pile of spaghetti crap? Have you ever seen anti-patterns like the following: Changing a line of code to fix screen A blows up screen B, which have no relation to each other. Many wrappers: A Service is wrapped by a Provider is wrapped by a Performer is wrapped by a ... It is hard to track down where is the code that performs a certain operation. Playing whack a mole, where each bug fix just yields a new bug. Ever ask yourself why OOP has design patterns? I would argue that OOP assumes upfront design before writing any code. In particular, OOP shines when every important thing is known at the outset. Take a Java List or Map as an example. They have remained virtually the same since the rollout of Java 1.2 when the collections API was added, replacing older classes like Vector and Dictionary. A List or Map is a simple beast - they are just ordered sets of data. A list orders items by index, a map by their keys. Once you have basic operations like add, change, iterate, and delete, what more do you really need? This is why Java has really only added conveniences like Map.computeIfAbsent, ConcurrentHashMap, and so on. Nothing huge, just some nice things that people were already doing anyway with their own convenience functions and/or classes. But custom software paid for by a customer who only knows what they want today is something altogether different. You literally don't know from one month to the next what feature the customer will ask for, or what bug they will report. Remember that OOP design pattern for random structural changes on a dime? Neither do I. Why Imperative Is Better OOP intrinsically means some kind of entanglement: once you pick a design pattern for a set of classes, and write a bunch of code for it, you can't easily change to some other design pattern. You can use different patterns for different sets of code, composing them as needed into a larger system. But each part is kind of locked into a chosen pattern, and it is a significant hassle to change the pattern later. It's like the coding equivalent of vendor lock-in. Unfortunately, a set of code doesn't necessarily shout out "Hey, this is the strategy pattern." You have to examine a set of code to reverse engineer its pattern or ask someone. Have you worked on a team that stated up front what patterns were being used for different parts of the system? I don't recall getting very much of this in my career. Really, in a lot of cases, there simply isn't any real conscious choice of design patterns, just replication of whatever the devs saw before elsewhere, often without any real contextual information of why. This entanglement easily leads to hard to deal with code if someone doesn't fully grok whatever pattern(s) are present. More often than not, using OOP for custom who-knows-what-the-customer-wants-next-week software is setting the system up for failure. Not failure as in it doesn't work, but failure as in it will virtually be guaranteed to become very hard to maintain. Using a simple imperative pattern is much better, which you can do even if the language is primarily OOP language like Java. In the case of Java, just use static methods, where each class corresponds to either a data structure or a series of static methods that operate on data structures. By passing data structures as arguments and returning new data structures, effectively the code is working from the outside, which makes the code simpler to understand and tends towards less entanglement. You could organize packages like this: Top-level packages represent functional areas (e.g., configuration, database access, REST API, validations, etc.) Sub-packages for data structures and functions that operate on them Some sub-packages can represent a design pattern like model, view, and controller For example, it might be organized like this, where app is the top-level dir checked out of the repo: app/db/util: Some utility functions to make DB access easier app/db/dto: Database transfer objects that represent data as stored/retrieved in the DB app/db/dao: Database access objects that store/retrieve dtos app/rest/util: Some utility methods to make REST a bit easier app/rest/view: Objects that represent the data as sent/received over HTTP app/rest/translate: Translate app/db/dto to /from app/rest/view app/rest/model: Make app/db/dao calls to store/retrieve data, uses /app/rest/{view, translate} app/rest/controller: Define endpoints and methods, use app/rest/model to do the work app/html: SSR HTML generation You'll notice I mention MVC above, which is an OOP pattern. However, this pattern can be simplified as a set of directories with one responsibility per directory, which can still be an imperative way of writing code. It can still be operating on the data objects from the outside. Just because we don't want to use OOP doesn't mean we can't apply some of what we've learned from it over the years in an imperative way. The above looks like a monolithic design. It can be a hybrid if you want: Make app/{service} dirs, which in turn contain DB, REST, and HTML as shown above. Each service can be its own application. Services can be grouped into a smaller number of deployments: you don't have to deploy each service in its own container. The Other Most Common Mistake One of the most important things to consider is (DO)RY versus (DONT)RY. The overuse of (DONT)RY is often a very big pain point in OOP. Like Lists and Maps, (DONT)RY works best in a limited area of code, such as reusing some common code across all Map implementations. Essentially, it is just another variation of what I said earlier about knowing the design in advance - (DONT)RY can be quite useful when you know the considerations up front, but just another factor in making spaghetti code when you don't. (DO)RY is far more useful when you have a changes-by-the-week application: the duplication isolates changes. For example, say you have a customer address and a business address. They seem kind of the same thing, with only minor differences: Businesses have 3 lines, for doors and stops and other cupboard-under-the-stairs things individuals don't need. Businesses can have multiple addresses, so they need a type (physical, mailing, billing). It sounds like you could use the same code for both. But over time, random requests are made for random changes, and some changes need to only apply to one or the other address type. (DONT)RY causes these increasing differences to get harder and harder to manage, which is exactly the bad form of entanglement I keep seeing. (DO)RY means copying code when a change needs to be done for both. The improvement stems from the fact that when a particular change must be implemented quite differently due to their differing code bases, there is no tangled mess problem - instead, it is just more effort to do the change twice in different ways, without causing either code base to become any harder to read or modify. In some cases, a data type that has its own logic for persistence and retrieval/display might also be contained inside another data type for another use case. When contained, there is no reason to believe in the face of random changes that it will necessarily always require the same validations, persistence, and display logic as when it is used as a top-level object. As such, all the logic for the contained object should be a copy of the top-level code, so the two use cases can be as different as they need to be. Conclusion Imperative programming combined with (DO)RY encourages making separate silos for each data type - separate queries, separate DB reads/writes, separate REST endpoints, and separate HTML generation. This separation expresses an important truth I alluded to earlier about your data - every top-level data type is completely unrelated to any other data type, it is a thing unto itself. Any correlation or similarity in two separate data types should be viewed as both accidental and temporal: in other words, they just so happen to be similar at the moment - there is no reason to believe their similarity will continue in the face of random unknowable future changes. Separating all your top-level data types with their own imperative code and using (DO)RY - copying code as necessary to maintain the separation - is the key to managing code that has to be dynamic in response to frequent unknowable future changes. The resulting code will be larger as a result of copying logic, but more maintainable. In other words, everything in programming is a trade-off, and the combination of imperative and (DO)RY is the best trade-off that results in more total code, but more maintainable code.
As the software development landscape continues to evolve at a rapid pace, Java stands out as a foundational language that drives a multitude of applications on a global scale. In 2024, the role of a Java software architect has assumed unprecedented significance. Software architects must not only possess a profound comprehension of Java and its ecosystem but also remain current with the latest trends, technologies, and best practices in order to construct resilient, scalable, and efficient applications. This article meticulously examines 20 essential areas that every Java software architect should aim to master in 2024. Encompassing diverse topics such as microservices, cloud-native applications, reactive programming, and blockchain technology, these areas encapsulate the requisite skills and knowledge crucial for navigating the ever-changing realm of software architecture. Furthermore, each section provides insights into related technologies and recommends pertinent books to furnish architects with a comprehensive roadmap for remaining at the forefront of their field. The Premium Java Programming Certification Bundle* *Affiliate link. See Terms of Use. 1. Microservices Architecture Adopting a microservices architecture entails reimagining applications as a collection of smaller, independently deployable services that are loosely coupled. This approach allows for individual development and scaling of services. Proficiency in this architectural style is essential for contemporary Java architects, as it facilitates the effective design and maintenance of robust, scalable, and resilient systems. Related Technologies Spring Boot A robust framework for creating stand-alone, production-grade Spring applications that you can "just run." Spring Cloud: Provides tools for developers to quickly build some of the common patterns in distributed systems (e.g., configuration management, service discovery, circuit breakers). Spring Data: Simplifies data access, making it easier to work with various databases and storage technologies. Spring Security: Comprehensive security services for Java applications, including authentication, authorization, and other security features. Quarkus Designed for Kubernetes and optimized for GraalVM and OpenJDK, Quarkus provides fast startup times and a low memory footprint. Quarkus Extensions: Enhancements and integrations with various technologies like Hibernate, RESTEasy, and Kafka. Panache: Simplifies the development of data access layers, making it easier to work with databases. Qute: A templating engine for Quarkus, enabling dynamic content rendering. OpenShift A Kubernetes-based platform that helps manage and deploy containerized applications, simplifying the orchestration of microservices. OpenShift Service Mesh: Integrates Istio, Jaeger, and Kiali to manage microservices traffic flow, observability, and tracing. OpenShift Pipelines: Based on Tekton, it provides a Kubernetes-native CI/CD framework to automate deployments. OpenShift Serverless: Based on Knative, it offers a serverless experience to build and deploy applications on demand. Recommended Books "Building Microservices: Designing Fine-Grained Systems" by Sam Newman "Spring Microservices in Action" by John Carnell "Quarkus Cookbook: Kubernetes-Native Java" by Alex Soto Bueno and Jason Porter 2. Cloud-Native Applications Developing robust applications that harness the full potential of cloud computing is imperative for businesses and organizations. This entails strategically leveraging cloud platforms and services to achieve seamless scalability, heightened reliability, and optimal operational efficiency. By effectively leveraging cloud computing, businesses can streamline their operations, enhance their agility, and facilitate cost-effective resource utilization. Related Technologies AWS: Amazon Web Services offers a comprehensive suite of cloud services. Google Cloud Platform: Provides a range of computing, storage, and application services. Microsoft Azure: Another leading cloud platform with extensive tools for building and managing applications. Recommended Books "Cloud Native Java: Designing Resilient Systems with Spring Boot, Spring Cloud, and Cloud Foundry" by Josh Long and Kenny Bastani 3. Containerization and Orchestration Mastering containerization and orchestration technologies ensures applications run smoothly across different environments, enhancing scalability and reliability. Related Technologies Docker: Enables you to package and run applications in containers. Kubernetes: An open-source system for automating deployment, scaling, and management of containerized applications. OpenShift: Extends Kubernetes with DevOps tools to facilitate container orchestration and management. Recommended Books "Docker: Up & Running" by Karl Matthias and Sean P. Kane "Kubernetes Up & Running" by Kelsey Hightower, Brendan Burns, and Joe Beda 4. Reactive Programming Reactive programming allows for handling asynchronous data streams effectively, which is crucial for modern web applications. Related Technologies Project Reactor: A foundational library for building reactive applications on the JVM. Akka: Toolkit and runtime for building highly concurrent, distributed, and resilient message-driven applications. RxJava: A library for composing asynchronous and event-based programs using observable sequences. Recommended Books "Reactive Programming with RxJava" by Tomasz Nurkiewicz and Ben Christensen "Reactive Spring" by Josh Long 5. Serverless Computing Serverless architecture enables you to build applications without managing infrastructure, improving agility and reducing operational overhead. Related Technologies AWS Lambda: A service that lets you run code without provisioning or managing servers. Azure Functions: A solution for easily running small pieces of code or "functions" in the cloud. Google Cloud Functions: Lightweight, event-based asynchronous compute solutions. Recommended Books "Serverless Architectures on AWS" by Peter Sbarski "Building Serverless Applications with Python" by Mohamed Labouardy 6. Event-Driven Architecture Designing systems that react to events in real time enhances scalability and responsiveness, making them ideal for modern applications. Related Technologies Apache Kafka: A distributed event streaming platform capable of handling trillions of events a day. RabbitMQ: A reliable messaging system that supports multiple messaging protocols. AWS SNS/SQS: Simple Notification Service (SNS) and Simple Queue Service (SQS) for scalable message queueing. Recommended Books "Designing Event-Driven Systems" by Ben Stopford "Kafka: The Definitive Guide" by Neha Narkhede, Gwen Shapira, and Todd Palino 7. Security Best Practices Implementing robust security measures to protect applications from threats and vulnerabilities is paramount for any architect. Related Technologies Spring Security: A powerful and customizable authentication and access control framework. OWASP Tools: Various tools and resources from the Open Web Application Security Project. JWT (JSON Web Tokens): A compact, URL-safe means of representing claims to be transferred between two parties. Recommended Books "Spring Security in Action" by Laurentiu Spilca "Java Security: Writing Secure Code" by Scott Oaks 8. DevOps and CI/CD Integrating development and operations through DevOps practices and implementing CI/CD pipelines is crucial for efficient and reliable software delivery. Related Technologies Jenkins: An open-source automation server that supports building, deploying, and automating projects. GitLab CI/CD: Provides robust CI/CD pipeline support integrated with GitLab. Travis CI: A continuous integration service used to build and test projects hosted on GitHub. Recommended Books "The DevOps Handbook" by Gene Kim, Patrick Debois, John Willis, and Jez Humble "Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation" by Jez Humble and David Farley 9. APIs and Integration Designing robust APIs for service integration ensures seamless communication between different systems, essential for microservices and hybrid cloud environments. Related Technologies REST: An architectural style for designing networked applications. GraphQL: A query language for your API, and a runtime for executing those queries by using a type system you define for your data. OpenAPI/Swagger: Tools for designing, building, documenting, and consuming RESTful web services. Recommended Books "Designing Web APIs" by Brenda Jin, Saurabh Sahni, and Amir Shevat "GraphQL: A Practical Guide with Examples" by Marc-Andre Giroux 10. Data Management and NoSQL Databases Handling large volumes of data effectively and understanding NoSQL databases is critical for performance and scalability. Related Technologies MongoDB: A document database with the scalability and flexibility that you want with the querying and indexing that you need. Cassandra: A distributed NoSQL database management system designed to handle large amounts of data across many commodity servers. Redis: An in-memory data structure store, used as a distributed, in-memory key–value database, cache, and message broker. Recommended Books "NoSQL Distilled: A Brief Guide to the Emerging World of Polyglot Persistence" by Pramod J. Sadalage and Martin Fowler "MongoDB: The Definitive Guide" by Kristina Chodorow 11. Distributed Systems Designing and managing distributed systems ensures high availability and fault tolerance, which are crucial for large-scale applications. Related Technologies Apache Zookeeper: A centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services. Consul: Provides service discovery, configuration, and segmentation functionality. Netflix Eureka: A REST-based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers. Recommended Books "Designing Data-Intensive Applications" by Martin Kleppmann "Distributed Systems: Principles and Paradigms" by Andrew S. Tanenbaum and Maarten Van Steen 12. Concurrency and Parallelism Efficiently managing concurrency and parallelism improves application performance and responsiveness, making it a critical skill for architects. Related Technologies Java Concurrency Framework: Provides high-level concurrency constructs. Fork/Join Framework: Simplifies the process of developing parallel applications. Reactive Streams: An initiative to provide a standard for asynchronous stream processing with non-blocking backpressure. Recommended Books "Java Concurrency in Practice" by Brian Goetz "Concurrency in Practice with Java" by Heinz Kabutz 13. Performance Tuning and Optimization Regularly tuning and optimizing Java applications ensures they run efficiently under different conditions and scales. Related Technologies Java Mission Control: A suite of tools for monitoring, managing, and troubleshooting Java applications. VisualVM: A visual tool integrating several command-line JDK tools and lightweight profiling capabilities. JProfiler: A powerful profiler for Java that helps troubleshoot performance bottlenecks. Recommended Books "Java Performance: The Definitive Guide" by Scott Oaks "Optimizing Java" by Benjamin J. Evans, Jim Gough, and Chris Newland 14. Understanding Java Ecosystem and Updates Keeping up with the latest Java updates and the ecosystem ensures using the most efficient and secure versions. Related Technologies JDK 17+: The latest long-term support (LTS) version of Java. OpenJDK: The free and open-source implementation of the Java Platform, Standard Edition. Recommended Books "Modern Java in Action" by Raoul-Gabriel Urma, Mario Fusco, and Alan Mycroft "Effective Java" by Joshua Bloch 15. Architectural Patterns and Best Practices Applying proven architectural patterns and best practices leads to more robust and maintainable applications. Related Technologies MVC (Model-View-Controller): An architectural pattern commonly used for developing user interfaces. CQRS (Command Query Responsibility Segregation): A pattern that separates read and update operations for a data store. Event Sourcing: Stores the state of a business entity as a sequence of state-changing events. Recommended Books "Patterns of Enterprise Application Architecture" by Martin Fowler "Domain-Driven Design: Tackling Complexity in the Heart of Software" by Eric Evans 16. Testing and Test-Driven Development (TDD) Implementing thorough testing practices and embracing TDD enhances code quality and reliability. Related Technologies JUnit: A simple framework to write repeatable tests. It is an instance of the xUnit architecture for unit testing frameworks. Mockito: A mocking framework for unit tests in Java. Selenium: A portable framework for testing web applications. Recommended Books "Test-Driven Development: By Example" by Kent Beck "JUnit in Action" by Petar Tahchiev, Felipe Leme, Vincent Massol, and Gary Gregory 17. Graph Databases Understanding and using graph databases enables efficient handling of highly connected data, which is increasingly important in modern applications. Related Technologies Neo4j: A highly scalable native graph database, purpose-built to leverage data relationships. Amazon Neptune: A fully managed graph database service. ArangoDB: A native multi-model database system. Recommended Books "Graph Databases" by Ian Robinson, Jim Webber, and Emil Eifrem "Learning Neo4j" by Rik Van Bruggen 18. Big Data and Analytics Leveraging big data technologies and analytics tools is essential for extracting valuable insights from large datasets. Related Technologies Apache Hadoop: An open-source framework that allows for the distributed processing of large data sets across clusters of computers. Apache Spark: A unified analytics engine for large-scale data processing. Elasticsearch: A distributed, RESTful search and analytics engine. Recommended Books "Big Data: Principles and Best Practices of Scalable Real-Time Data Systems" by Nathan Marz and James Warren "Spark: The Definitive Guide" by Bill Chambers and Matei Zaharia 19. Artificial Intelligence and Machine Learning Integrating AI and ML capabilities into applications can offer competitive advantages and new functionalities. Related Technologies TensorFlow: An open-source library for machine learning. Deeplearning4j: A deep learning library for the JVM. Weka: A collection of machine learning algorithms for data mining tasks. Recommended Books "Artificial Intelligence: A Guide for Thinking Humans" by Melanie Mitchell "Deep Learning with Java" by Yusuke Sugomori 20. Blockchain Technology Understanding blockchain fundamentals and its potential applications can open new possibilities for secure, decentralized applications. Related Technologies Hyperledger Fabric: A permissioned blockchain infrastructure. Ethereum: A decentralized platform that runs smart contracts. Corda: An open-source blockchain platform designed for business. Recommended Books "Blockchain Basics: A Non-Technical Introduction in 25 Steps" by Daniel Drescher "Mastering Blockchain" by Imran Bashir Conclusion The role of a Java software architect is becoming increasingly vital as the software development landscape continues to undergo rapid evolution. In this article, we emphasize the critical knowledge and skills that are indispensable for successfully navigating the intricacies of modern application development. Java architects must not only be proficient in mastering microservices architecture, cloud-native applications, containerization, reactive programming, serverless computing, event-driven architecture, and robust security practices, but they must also understand the interplay between these concepts to ensure that their applications are highly resilient, scalable, and secure in today's dynamic environment. Remaining abreast of the latest technologies and best practices, and engaging in continuous learning through recommended books, is essential for architects to consistently design and construct high-performance applications that can meet the demands of today's digital world. By embracing these fundamental principles, architects not only elevate the quality of their software but also position themselves to steer innovation and lead their organizations into the future.
Nicolas Fränkel
Head of Developer Advocacy,
Api7
Shai Almog
OSS Hacker, Developer Advocate and Entrepreneur,
Codename One
Andrei Tuchin
Lead Software Developer, VP,
JPMorgan & Chase
Ram Lakshmanan
yCrash - Chief Architect