Java vs JavaScript | Friends or Enemies
--
In the modern IT industry, Java and JavaScript stand as leading programming languages for building scalable and reliable software systems. They offer flexibility and automation capabilities for implementing business rules. When combined with modern cloud solutions like Amazon Web Services and Google Cloud, Java and JavaScript become powerful tools for software engineers. In this article, I will explain how they can be used together, how knowledge sharing can occur between the Java and JavaScript communities, and the pros and cons of both languages when implementing business rules. 🚀
Allow me to introduce myself, as this article draws inspiration from my experience. I’ve spent many years working with JavaScript, using various libraries and frameworks like TypeScript, Node.js, Nest.js, Express.js, React.js, Angular, and more. Working with JavaScript has been an amazing experience due to its flexibility and the availability of versatile tools. However, I often felt that something was lacking in the JavaScript community, particularly in terms of engineering approaches.
As I delved into the world of software engineering, exploring topics like design patterns, architectural solutions, domain-driven design, and concurrency practices, I noticed an interesting pattern. Most books in these areas predominantly used Java for their examples, with very few focusing on JavaScript. This observation prompted me to explore the Java community more deeply.
I discovered that the Java community heavily emphasizes architectural approaches that facilitate the creation of highly scalable systems. Moreover, I observed that refactoring and maintaining a Java codebase were considerably easier compared to working with JavaScript, even when TypeScript was used. This led me to formulate an assumption: the Java community prioritizes learning approaches for building maintainable and scalable systems, starting from considerations of code quality and code styling, and most importantly, how APIs should be designed. This emphasis extends beyond just REST APIs and encompasses system layer APIs and interface APIs.
In this article, I aim to share my observations from exploring both the Java and JavaScript worlds. I will discuss intriguing concepts and practices that can be exchanged between these two communities. Currently, I am actively engaged in working with both Java and Node.js, and I find them to be powerful tools when used together. 🧑💻
Community
As previously mentioned, the community plays a crucial role in shaping the evolution of programming languages and best practices. In my experience, some pain points revolve around API design, error handling, and layer decomposition. JavaScript, by its nature, introduces certain tricky features like undefined, null, and NaN. Therefore, it becomes vital to consistently consider these aspects when designing meaningful APIs.
Exception handling is another significant topic, well-established in the Java world. Unfortunately, in the JavaScript realm, it doesn’t hold the same prominent position, leading to potential unexpected behavior in software systems. As I highlighted earlier, dealing with JavaScript’s tricky features is essential. While TypeScript can aid in addressing type-safety issues, it’s crucial to handle runtime behaviors carefully. For instance, when passing values like undefined, null, or NaN to a method that expects a numeric type, unexpected exceptions may arise. Even more concerning, unexpected outcomes can be written to the database, potentially causing serious production issues.
const convertToDate = (milliseconds: number): Date {
// some impmenetation here
};
// What's the expected outputs?
convertToDate(null);
convertToDate(undefined);
convertToDate(NaN);
// You never know
The provided code snippet effectively illustrates the potential issues that may arise. It remains uncertain whether the result will be null, an instance of a Date with an arbitrary value, or an instance of a Date with an ‘invalid date’ value. The final outcome largely depends on the implementation of the convertToDate
API.
In cases where strict business requirements do not allow for invalid values, it becomes evident that validation should be employed. In the Java community, the ‘fail fast’ approach is widely adopted, and frameworks like Spring Core provide mechanisms such as the Assert static class for rigorously validating inputs. This proactive approach to validation helps identify and address issues at the earliest possible stage, contributing to more robust and reliable software systems.
Class DateTimeConverter {
public static Date convertToDate(Long milliseconds) {
Assert.notNull(milliseconds, "milliseconds can not be null");
Assert.isTrue(milliseconds > 0, "milliseconds can not be less or equal to zero");
// some implmentation here
}
}
class Main {
public static void main(String[] args) {
// What's the expected outputs?
DateTimeConverter.convertToDate(null); // IllegalArgumentException
DateTimeConverter.convertToDate(0L); // IllegalArgumentException
DateTimeConverter.convertToDate(1695046085257L); // Expected output
}
}
With assertions and meaningful exceptions, engineers can promptly identify issues and understand that corrections are needed. The ‘fail fast’ approach also helps prevent unexpected outputs from being accepted as correct results. Moreover, concepts like the null object pattern and SOLID principles, essential for building maintainable software, are not widely adopted in the JavaScript community.
These observations underscore the importance of cultivating communities that promote architectural and design patterns as common practices and mindsets. Additionally, there is a need for more literature, including books and articles, that explore software engineering principles using JavaScript as an illustrative example.
What distinguishes the JavaScript community is the engineers’ enthusiasm for delving deep into JavaScript’s internal workings. They aim to comprehend how everything operates at a C/C++ level and how to write optimized code to avoid inline caching revocations. In my view, the Java community often does not delve as deeply into internal implementations, mainly because Java’s AOT and JIT compilations address and optimize many performance-sensitive aspects. It would be beneficial for the community to encourage a standard of exploring Java’s internal implementation in greater depth. 🎉
Technical Restrictions
It’s evident that technical restrictions can significantly influence your decision-making process when choosing a programming language and determining how to write and optimize your code. I’d like to elaborate on the primary technical constraints that apply to both programming languages and discuss their potential impact on their usage. The primary distinction between these languages lies in their approach to programming paradigms: Object-Oriented Programming (OOP) vs. Functional Programming.
JavaScript primarily promotes functional programming as its main programming model, offering features like function compositions, Higher-Order Functions, aggregate functions, closures, and more. Implementing these features in Java can be a bit more challenging compared to JavaScript because JavaScript simplifies working with function references. It excels in constructing pipelines using patterns such as Filter, Pipe, and Transform, allowing for flexible and dynamic strategies for filtering, piping, and transformation that can be changed at runtime.
In contrast, Java predominantly follows the Object-Oriented Programming (OOP) paradigm. Java provides the capability to implement all OOP paradigms directly without the need for workarounds. One of Java’s standout features is its support for interfaces. While TypeScript also offers interfaces, JavaScript interfaces lack runtime metadata. In Java, interfaces allow engineers to precisely define contracts between components and establish clear boundaries between different domains. Furthermore, leveraging OOP makes it straightforward and maintainable to define business entities, business use cases, and bounded contexts. To illustrate, let’s consider two modules: User and Book. Although User and Book represent distinct domain entities, users can aggregate their own books. Let’s define the API for the Book module.
// Public API
public interface BookRepository {
Collection<Book> findByUserId(String userId);
}
// Package level implementation
class DefaultBookRepository implements BookRepository {
@Override
public Collection<Book> findByUserId(String userId) {
// Some implementation
}
}
Using the interface as a contract enables us to rely solely on the interface as a dependency for the User module. The Dependency Injection (DI) container then takes charge of determining which implementation to inject into the User module. This approach effectively segregates the implementation from the public API, with the interface serving as a dependency injection token. Consequently, we can effortlessly mock the implementation without having to worry about the intricacies of mocking frameworks.
It’s important to note that achieving the same behavior in JavaScript or TypeScript requires more adjustments due to their inherent differences. In TypeScript, one workaround is to use an abstract class without any implementation, which can act as a dependency injection token. However, this workaround has its complexities, such as the risk of someone adding an implementation to the abstract class, which could lead to unintended consequences.
JavaScript’s inherent design as a functional programming language allows you to create numerous pipelines with various strategies efficiently. With just a few lines of code, you can implement high-order functions, function compositions, selectors, and more.
For instance, suppose you wish to dynamically add a multiplication function to a stream of numbers and then append an addition function. You can achieve this with minimal code:
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
const compose = (f, g) => (x) => f(g(x));
const addThenMultiply = compose(multiply, add);
const result = addThenMultiply(2, 3);
This code demonstrates the flexibility and ease with which you can manipulate functions in JavaScript’s functional programming paradigm.
The most significant distinction between Java and JavaScript lies in their multithreading support. I’ll delve into this topic in greater detail in the usage section. ✨
Usage
Determining whether to use Java or JavaScript is not the primary challenge that needs addressing. Instead, the key lies in how you use both of these languages together. They can complement each other, concealing each other’s weaknesses, and leveraging both enables the creation of scalable and maintainable software solutions. Before delving further into the practical applications of both languages, let’s introduce a statement commonly expressed by professionals in the field.
Choosing a programming language is a business decision that impacts various aspects of a project’s success, from development costs and timelines to the overall competitive advantage and long-term sustainability of an organization. It involves careful consideration of technical and non-technical factors, alignment with business goals, and a proactive approach to managing risks and opportunities.
To illustrate this statement, consider the following scenario: A client expresses the need for banking system software. The client is aware that Java is widely adopted as the primary programming language for building banking applications due to its reputation for security. When you aim to pitch your software solution to this client, a natural question the client might ask is, “Is the software written in Java?” This emphasizes that the choice of programming language often hinges on the specific business domain in which the software operates.
It’s important to note that the traditional assumption that Java is multithreaded while JavaScript is single-threaded doesn’t fully apply in modern technology environments. For example, Java 21 introduced virtual threads, and Node.js 12 introduced worker threads. Additionally, with readily available scalable cloud solutions, we can scale server instances as needed.
Regarding the handling of a high volume of requests, Node.js excels due to its event loop, which can efficiently manage millions of concurrent requests, surpassing Java’s conventional multithreaded model. However, Java takes the lead in scenarios involving computationally intensive operations. In a multithreaded environment, Java can execute these operations without blocking the main thread, ensuring superior performance.
In my view, the optimal approach involves harnessing both Java and JavaScript. I’d like to make the following assumption:
If a component encompasses the business domain and business use cases, Java is an ideal choice due to its object-oriented programming (OOP) paradigm. On the other hand, if the component deals with tasks such as filtering, piping, and transforming a stream of data (e.g., CSV exporting) that involve asynchronous operations, JavaScript is well-suited because of its functional programming paradigm.
This practical example can be illustrated using AWS services. For dedicated services that need to remain continuously active, providing APIs for business domains and use case automation, deploying Java applications on EC2 instances can be a suitable choice. However, when you require functionalities such as chains of responsibilities, CSV exporting, Parquet file writing, or notification sending that need to operate asynchronously and trigger only when specific domain events occur, AWS Lambdas with Node.js can be a fitting solution. 🤌
Conclusion
Designing a scalable system is undoubtedly a challenging endeavor. However, with the availability of modern technologies and cloud solutions, the process of building scalable and reliable systems has become more accessible. The critical factor for success lies in the judicious use of tools and technologies and harnessing their synergy. Java and JavaScript can indeed be valuable allies, working in tandem to address various tasks. This combination can simplify your development journey significantly.
Having experienced both technologies and their respective communities, I would like to highlight a few key points that should be taken into consideration:
- Knowledge Sharing Between Communities: Encouraging the exchange of knowledge and ideas between the Java and JavaScript communities is crucial. Each community has valuable insights and practices that can benefit the other. Promoting cross-pollination of ideas can lead to improved software engineering practices and solutions.
- Using Technologies That Will Help Each Other: Consider leveraging Java and JavaScript in a way that complements their strengths. Identify where each language excels and use them accordingly. This approach can lead to more efficient and effective solutions, especially when combining the OOP strengths of Java with the functional programming capabilities of JavaScript.
- Reasonable Usage of Technologies: Exercise prudence when choosing which technologies to employ for specific tasks. Carefully evaluate whether Java or JavaScript is the more suitable tool for a particular job. Strive for a balance that maximizes the advantages of both languages while mitigating their respective limitations.
Indeed, it’s crucial to emphasize the importance of considering design patterns and architectural principles when working with Java and JavaScript or any technology stack. Deepening your understanding of these main technologies and applying design patterns appropriately can greatly benefit your software development efforts. By using each technology for its intended purpose and fostering collaboration between them, you can harness their full potential to create powerful systems. The key is to make informed decisions and ensure that your chosen technologies work in harmony to simplify the development process and enable you to build a wide range of systems effectively. 🧨
Thank you for taking the time to read :)