E-Commerce Development Essentials: Considering starting or working on an e-commerce business? Learn how to create a backend that scales.
Low-Code Development: Learn the concepts of low code, features + use cases for professional devs, and the low-code implementation process.
Postgres Full-Text Search With Hibernate 6
Unleash Peak Performance in Java Applications: Overview of Profile-Guided Optimization (PGO)
Enterprise Security
This year has observed a rise in the sophistication and nuance of approaches to security that far surpass the years prior, with software supply chains being at the top of that list. Each year, DZone investigates the state of application security, and our global developer community is seeing both more automation and solutions for data protection and threat detection as well as a more common security-forward mindset that seeks to understand the Why.In our 2023 Enterprise Security Trend Report, we dive deeper into the greatest advantages and threats to application security today, including the role of software supply chains, infrastructure security, threat detection, automation and AI, and DevSecOps. Featured in this report are insights from our original research and related articles written by members of the DZone Community — read on to learn more!
API Integration Patterns
Getting Started With Low-Code Development
In this text, the final summary of my categories theory series, I will use my spotlight and take a closer look at the relations between all three previously described functional containers, namely: Functor, Monad, and Applicative. Below, you will find a comparison of them in terms of: Theory Laws Methods Possibilities and use cases Theory Functor In category theory, a Functor represents the mapping between two categories. In software, Functors can be viewed as a util class that allows us to perform a mapping operation over values wrapped in some context. It can also be viewed as an interface proving a certain ability, namely the ability to transform the state of an object. In more functional-focused languages like Scala or Haskell, Functor is a typeclass. There are a few types of Functors. However, their in-depth description and analysis are quite out of the scope of this article, but do not be afraid: I added links to other resources that will help you deepen your knowledge. Functor types: Contravariant Functor Invariant Functor Opposite Functor Bifunctor and Multifunctor There are no good built-in counterparts of Functors in Java and other modern-day programming JVM languages. Nevertheless, they are reasonably easy to implement manually. Probably the best counterpart can be found in the Scala library called Cats or in Haskell. Applicative In category theory, an Applicative, also known as an Applicative Functor, is a concept that generalizes a Functor by allowing for operations not just on the values inside a context but also on functions within a context. It sits between Functors and Monads in terms of expressive power. While less powerful than Monads, Applicatives are more structured than Functors. Going by the book, Applicatives do not allow chain operations in the same ways as Monads do (with the output of one operation being the input of the other). On the other hand, unlike Functors, Applicatives allow us to sequence our computations. Unfortunately, such a relation is quite hard to achieve in the world of software as in the end, the underlay data will end up being used as input for the next. Monad In the world of software, we can view it as a wrapper that puts our value in some context and allows us to perform operations, specifically operations that return results wrapped in the Monadic context, on the value. Monads are mostly used to handle all kinds of side effects: Performing I/O operation Handling operations that can throw exceptions Handling async operations Another important point is that we can chain the operations in such a manner that the output of an operation at any step is the input to the operation at the next step. Such behavior allows us to write very declarative code with minimal boilerplate. Contrary to Functor, Monad has quite a lot of implementations built in modern-day programming languages: Scala (Option, Try, Either, etc.) Haskell (IO, etc.) Java (Optional - not exactly a Monad, but close) From both practical and theoretical points of view, it is also the most powerful of all the three concepts. The complete hierarchy of all three in terms of pure mathematical power looks more or less like this, going from least to most powerful: Laws Functor If we want to implement a Functor, our implementation needs to obey two laws: Identity and Associativity. Identity: Mapping values inside the Functor with the identity function should always return an unchanged value. Associativity: In the chain of function applications, it should not matter how functions are nested. Applicative If we want to implement Applicative, our implementation needs to obey four laws namely: Identity, Homomorphism, Interchange, Composition. Identity: Applying the identity function to a value inside the context should always return an unchanged value. Homomorphism: Applying a function in context to a value in context should give the same result as applying the function to the value and then putting the result in context with the usage of pure. Interchange: Applying the function with context f to a value with context should be the same as applying the wrapped function, which supplies the value as an argument to another function, to the function with context f. Composition: Applying the function with context f and then the function with context g should give the same results as applying functions composition of f and g together within the context. Alternative Applicative There is also an equivalent version of Applicative whose implementation needs to obey three laws: Associativity, Left Identity, and Right Identity. Left identity: If we create a new Applicative and bind it to the function, the result should be the same as applying the function to the value. Right identity: The result of binding a unit function to the Applicative should be the same as the creation of a new Applicative. Associativity: In the chain of function applications, it should not matter how functions are nested. If you would like to take a closer look at the laws of this version of Applicative you can notice that they are the same as the Functor laws. The change in the Identity Law originates from the difference in the setup of both concepts. In particular from the existence of the pure method in Applicative, in a way, we have to check one more case. Monad If we want to implement a Monad, we have to obey three laws: Left Identity, Right Identity, and Associativity. Left identity: If we create a new Monad and bind it to the function, the result should be the same as applying the function to the value. Right identity: The result of binding a unit function to a Monad should be the same as the creation of a new Monad. Associativity: In the chain of function applications, it should not matter how functions are nested. As in the case of Applicative, Monad laws are very similar to Functor laws. What is more, Monad laws are exactly the same as Applicative laws. The only difference is regarding the concept described in the Laws, the condition remains unchanged. The relation between the three, in terms of which concepts extend which, will look more or less like in the picture below. Basically, everything is a Functor. Methods To implement any of the structures, you will need a language with generics support as a parameterized type M<T> is a base for any of them. On the other hand, you can just generate the code for all required types via a macro or some other construct but with generics, it is significantly easier. Functor To implement Functor, you will have to implement only one method: map, you pass a function that operates on value in context. This method should have the following signature M<R> (T -> R). Applicative To implement Applicative you will have to implement two methods: pure, which is used to wrap your value in the Applicative context and has the following signature: M<T>(T). apply (ap): You pass a function with context, and the function then operates on the value in the context. This method should have the following signature: M<U> (M<T -> U>). Alternative Applicative There is also an equivalent version of Applicative where we are using the product instead of apply. product: You pass two different values wrapped in Applicative context and get context with both values in return. This method should have the following signature: M<(T, U)>(M<T>, M<U>). In both approaches to Applicatives, you get a map method with signature M<R> (T -> R) by their definition as all Applicatives are Functors. Monad To implement Monad you will have to implement two methods: unit (of), which is used to wrap the value in Monad context and has the following signature: M<T>(T). bind (flatMap): You pass a function that operates on value in context but the result is already wrapped in Monad context. This method should have the following signature: M<U> (T -> M<U>). What is more, Monad is also an Applicative and, by extension, a Functor. It means that we are getting a lot of additional methods for free. In particular, a map method from Functor and apply or product methods from Applicative. The Possibilities and Use Cases Functor The Functor is just a util method. It does not provide anything more than simple mapping of a value inside an arbitrary context. On the other hand, it is also the simplest and the easiest of all three concepts and can be used almost anywhere where we need to perform operations over the values in the context. Thought Functors still have their limitations. By its definition, Functor is unable to chain computations. What is more, Functor does not provide a way to flatten the result of performed operations, so we can easily end up with nested types like <List<List<Long>>. However, the possibility of performing effective operations without moving them out of context is quite a good thing. Applicative Going further, we have an Applicative with which we can apply functions in context to a value with context. Additionally, with its product version, we can create a tuple out of two objects within the same context. Such behavior can be extremely useful in some cases: we can use it to compose multiple effects to one effect, reducing types like List<Future> to more reasonable Future<List>. That is the reason why Applicatives are a great choice for implementing concepts like parsers, traversables, or composers and any other use case where we need to work with many independent effects of the same type. What is more, because the Applicative is a Functor we can also apply some operations on such output tuples via the map method. Monad Last but not least, we have Monad – the most powerful of all three structures. Each Monad implementation represents an effect: Emptiness (Option) Possible failure (Try) Result or failure of an operation (Either) Chain of computations (IO) What is more, Monad gives us a way to put values in such an effective context. Furthermore, thanks to the flatMap method, we can handle nested return types. This in turn solves the issues with M<M<T>> styled types (Optional<Optional<Long>). Monad implementation will automatically flatten such types to have only one effect – an operation that the map method from Functor is unable to make by its definition. Additionally, with Monads, we can perform contextual operations as we can pass outputs of one operation as inputs to another achieving a nice, declarative chain of computations. Summary Functional containers are a very useful tool that can be applied to a variety of problems. In my opinion, we should find them a place in our engineering toolbox or at least be aware of them. Just please remember that they are not a silver bullet, and making everything a Monad may not be the best way to write your code. If you would like to deepen your knowledge of a particular container, here is the list with my articles describing all three of them in more detail. What Is a Functor? Basic Theory for Java Developers What Is Applicative? Basic Theory for Java Developers What Is a Monad? Basic Theory for a Java Developer As a side note, I would like to add that these containers add another nice touch of math to the world of software engineering, which is another good point to like them, at least in my opinion. Hope that you find my text interesting. Thank you for your time. Review by Mateusz Borek
Hit Pause! I've been on break for the past few days, completely unplugged from work. It's been a time of reflection, diving into fiction, and sometimes simply sitting and doing nothing. I've wandered through various Christmas markets and taken a few spontaneous day trips to nearby towns, enjoying holiday cheer. Sure, at times, I felt physically tired, but never exhausted. It made me realize how crucial it is to hit pause. In our hectic day-to-day lives, we constantly find ourselves making decisions. From the moment we wake up — deciding on breakfast, travel to the office or work from home, and so on — to big decisions such as where to attend university, which car to buy, or where to live, each carrying its weight. Internet sources claim that we make roughly whooping 35,000 decisions a day! If we assume an adult sleeps for eight hours, thankfully decision-free, that amounts to more than 2,100 decisions per waking hour or about three decisions every five seconds. Tech World of Constant Choices In recent years, the software realm has experienced exponential growth and acceleration. Being an IT pro means mastering multiple tools, navigating diverse platforms, operating within larger teams, and meeting heightened client expectations to turn those innovative ideas into life. Take, for example, full-stack developers today — they are juggling front-end technologies, backend languages, databases, cloud solutions, web architecture, versioning, testing methodologies, and Agile approaches. And guess what? That's just scratching the surface! Diving into the front end alone, like choosing a JavaScript solution, you are filtering through numerous frameworks, each necessitating careful selection and combination for optimal outcomes. But wait, it does not end there, and the technology continually evolves, with new frameworks and significant updates emerging almost daily, making it nearly impossible to make a perfect decision. We, tech professionals, encounter essential decisions that demand cohesive teamwork to devise strategies, deliver secure, scalable solutions, and visualize development roadmaps. These decisions necessitate focused efforts like workshops or sessions. Then, we have routine tasks such as coding, refining, and documenting, which need constant thinking. Amidst these concrete decisions, we must make countless tiny choices, often termed micro-decisions. Should I respond to this Slack message now? Answer this call or let it ring? Must I attend this meeting? Does this email need an immediate reply? With our workplaces all tangled up in a web of apps, these tiny choices sometimes trip us up, disrupting our workflow and elevating stress levels. The Cost of Choice: Decision Fatigue Decision Fatigue isn't a new concept; it was introduced by social psychologist Roy F. Baumeister. Every decision has a cost, tapping into our limited mental decision-making power. Keep making choices, and the strain piles up. Psychology professors Michael Inzlicht and Brandon Schmeichel suggested that there's a cap to what we can accomplish in a day. Consequently, as our mental energy dwindles, it starts impacting our decision-making, specifically challenging choices. Instead of the most effective solution, we opt for a known effortless approach because the mental toll is lower. I believe every developer has encountered one-dimensional reasoning — decisions that seemed sound initially but led to incidents in production. I'm no exception. I work in a platform team that creates and maintains shared business service libraries. I've seen scenarios where the team caught up in endless meetings and rushed changes towards the day's end. Only to realize later that it suited only the team that asked for it, a quick fix. Instead, we needed a robust, more comprehensive, generalized, and future-proof solution that required much more effort and thoughtful decisions to benefit everyone. As decision-making fatigue mounts, so does stress. This piled-up stress can lead to burnout, affecting our job performance negatively. But it does not stop at work; it also seeps into our personal lives. From Overwhelmed To Optimized To ensure we are operating at our best, we must either avoid decision fatigue or design ways to recover. World leaders like Obama and Zuckerberg have their strategies to combat decision fatigue, keeping things simple with daily routines that preserve mental energy for critical decisions. Stealing their proven techniques could serve as an excellent starting point. But apart from holding the mental energy, we can also refill it by taking regular breaks, especially when we notice signs of fatigue. I actively keep an eye out for these signals. For example, whenever I find myself easily distracted during work, I know something is going wrong. If I am checking phone notifications or skimming through news headlines more than I should, it is a clear sign that I have lost interest. Forcing myself to power through the task despite a lack of interest will drain my willpower unnecessarily. It is a clear signal for me to pause, take a breather, glance out the window, and get some fresh air. Another effective method for me has been the Pomodoro Technique. It's all about laser-focused work intervals. I lock into a 25-minute Google timer session, shutting down all notifications to banish distractions. No emails or Slack pings — just me and the task at hand. Then, after that intense session, I take a quick 5-minute breather. Trust me, after these power-packed sessions, I've consistently found myself more productive and less drained. Final Words The research and articles I referenced above suggest interesting hacks like avoiding late-day decision-making or simplifying decisions such as having a wardrobe with identical plain t-shirts and suits. But hey, not all studies have the same findings. For instance, in a study led by Stanford psychologist Carol Dweck and her colleagues, she concluded that waning willpower was apparent only in participants who believed willpower was a finite resource. Decision-making isn't a one-size-fits-all deal. While cutting down on small choices can save time and enhance efficiency, rigid routines might feel monotonous, confining, or impossible for some. Regardless, buying into the idea of limited decisiveness or willpower probably won't do us any favor. Instead, I would say acknowledging that mental energy is a depleting resource and can drain over time, like a muscle that fatigues with exercise, can be practical to avoid exhaustion. I wish you more power in the year 2024. Happy New Year!
Meetings are a crucial aspect of software engineering, serving as a collaboration, communication, and decision-making platform. However, they often come with challenges that can significantly impact the efficiency and productivity of software development teams. In this article, we will delve deeper into the issues associated with meetings in software engineering and explore the available data. The Inefficiency Quandary Meetings are pivotal in providing context, disseminating information, and facilitating vital decisions within software engineering. However, they can be inefficient, consuming a substantial amount of a software engineer’s workweek. According to Clockwise, the average individual contributor (IC) software engineer spends approximately 10.9 hours per week in meetings. This staggering figure amounts to nearly one-third of their workweek dedicated to meetings. As engineers progress in their careers or transition into managerial roles, the time spent in meetings increases. One notable observation is that engineers at larger companies often find themselves in even more meetings. It is commonly referred to as the “coordination tax,” where the need for alignment and coordination within larger organizations leads to a higher volume of meetings. While these meetings are essential for keeping teams synchronized, they can also pose a significant challenge to productivity. The Cost of Unproductive Meetings The impact of meetings on software engineering extends beyond time allocation and has financial implications. Research by Zippia reveals that organizations spend approximately 15% of their time on meetings, with a staggering 71% of those meetings considered unproductive. It means that considerable time and resources invested in discussions may not yield the desired outcomes. Moreover, unproductive meetings come with a substantial financial burden. It is estimated that businesses lose around $37 billion annually due to unproductive meetings. On an individual level, workers spend an average of 31 hours per month in unproductive meetings. It not only affects their ability to focus on critical tasks but also impacts their overall job satisfaction. The Impact on Software Engineering In the realm of software engineering, the inefficiencies and challenges associated with meetings can have several adverse effects: Delayed Development: Excessive or unproductive meetings can delay project timelines and hinder software development progress. Reduced Productivity: Engineers forced to spend a significant portion of their workweek in meetings may struggle to find uninterrupted “focus time,” which is crucial for deep work and problem-solving. Resource Drain: The coordination tax imposed by meetings can strain resources, leading to increased overhead costs without necessarily improving outcomes. Employee Morale: Prolonged or unproductive meetings can decrease job satisfaction and motivation among software engineers. Ineffective Decision-Making: When meetings are not well-structured or attended by the right participants, critical decisions may be postponed or made without adequate information. Meetings are both a necessity and a challenge in software engineering. While they are essential for collaboration and decision-making, the excessive time spent in meetings and their often unproductive nature can hinder efficiency and impact the bottom line. In the following sections, we will explore strategies to address these challenges and make meetings in software engineering more effective and productive. The Benefits of Efficient Technical Meetings in Software Engineering In the fast-paced world of software engineering efficient technical meetings can be a game-changer. They are the lifeblood of collaboration, problem-solving, and decision-making within development teams. In this article, we’ll explore the advantages of conducting efficient technical meetings and how they can significantly impact the productivity and effectiveness of software engineering efforts. Meetings in software engineering are not mere formalities; they are essential forums where ideas are exchanged, decisions are made, and project directions are set. However, they can quickly become a double-edged sword if not managed effectively. Inefficient meetings can drain valuable time and resources, leading to missed deadlines and frustrated teams. Efficiency in technical meetings is not just a buzzword; it’s a critical factor in the success of software engineering projects. Here are some key benefits that efficient meetings bring to the table: Time Savings: Efficient meetings are succinct and stay on topic. It means less time spent in meetings and more time available for actual development work. Improved Decision-Making: When meetings are focused and well-structured, decisions are made more swiftly, preventing bottlenecks and delays in the development process. Enhanced Collaboration: Efficient meetings encourage active participation and open communication among team members. This collaboration fosters a sense of unity and collective problem-solving. Reduced Meeting Fatigue: Prolonged, unproductive meetings can lead to fatigue, hindering team morale and productivity. Efficient meetings help combat this issue. Knowledge Sharing: With a focus on documentation and preparation, efficient meetings facilitate the sharing of insights and knowledge across the team, promoting continuous learning. We will delve into a five-step methodology to achieve these benefits to make technical discussions more efficient. While not a silver bullet, this approach has proven successful in many scenarios, particularly within teams of senior engineers. This methodology places a strong emphasis on documentation and clear communication. It encourages team members to attend meetings well-prepared, with context and insights, ready to make informed decisions. By implementing this methodology, software engineering teams can balance the need for collaboration and the imperative of focused work. In the following sections, we will explore each step of this methodology in more detail, understanding how it can revolutionize the way software engineers conduct technical meetings and, ultimately, how it can drive efficiency and productivity within the team. Step 1: Context Setting The initial step involves providing context for the upcoming technical discussion. Clearly articulate the purpose, business requirements, and objectives of the meeting. Explain the reasons behind holding the meeting, what motivated it, and the criteria for considering it a success. Ensuring that all participants understand the importance of the discussion is critical. Step 2: Send Invitations With Context After establishing the context, send meeting invitations to the relevant team members. It is advisable to provide at least one week’s notice to allow participants sufficient time to prepare. Consider using tools like Architecture Decision Records (ADRs) or other documentation formats to provide comprehensive context before the meeting. Step 3: Foster Interaction To maximize efficiency, encourage collaborative discussions before the scheduled meeting. Share the ADR or relevant documentation with the team and allow them to engage in discussions, provide feedback, and ask questions. This approach ensures that everyone enters the meeting with a clear understanding of the topic and can prepare with relevant references and insights. Step 4: Conduct a Focused Meeting When it’s time for the meeting, maintain a concise and focused approach. Limit the duration of the meeting to no longer than 45 minutes. This time constraint encourages participants to stay on track and make efficient use of the meeting. Avoid the trap of allowing meetings to expand unnecessarily, as per Parkinson’s law. Step 5: Conclusion and Next Steps After the meeting, clearly define the decision that has been made and summarize the key takeaways. If the discussion led to a decision, conclude the Architecture Decision Record or relevant documentation. If further action is needed, create a list of TODO activities and determine what steps are required to move forward. If additional meetings are necessary, return to Step 2 and schedule them accordingly based on the progress made. By following these key steps, software engineering teams can streamline their technical discussions, making them more efficient and productive while preserving valuable product development and innovation time. This approach encourages a culture of documentation and collaboration, enabling teams to make informed decisions and maintain institutional knowledge effectively. Conclusion In the fast-paced world of software engineering, efficient technical meetings play a crucial role, offering benefits such as time savings, improved decision-making, enhanced collaboration, reduced meeting fatigue, and knowledge sharing. To harness these advantages, a five-step methodology has been introduced emphasizing documentation, clear communication, and preparation. By adopting this approach, software engineering teams can balance collaboration and focused work, ultimately driving efficiency, innovation, and productivity.
This article provides examples of hacking techniques that can help Java developers avoid vulnerabilities in their programs. It is not intended to train hackers but rather for naive developers who think that standard obfuscators will save them from their intellectual property theft. There will be logical gaps in the text, but only for brevity. Any experienced developer or hacker can easily complete them. All code examples are taken from real applications. Case 1: Authentication Server The Client-Server model appeared long before the Internet and is still used everywhere. At the same time, a myth arose that it is impossible to build software protection without the Client-Server part. Below, however, we will show that this is just a myth. The diagram showing the Server-Client data flows is as follows. Fragments of the program are outside for clarity. The so-called critical points are circled. What this is will become clear from what follows. The myth about the reliability of the authentication server is quite common, so we'll start with it. We have a server, which receives a request from a program and, after checking, sends a response: will the program continue to work, or is paid registration required? Let's find the point where we get a request simply by searching for classes for working with a LAN or the Internet, i.e., classes from package java.net.*, e.g., HttpURLConnection, and similar. Let us look at this diagram from our point of view, i.e., developer, and then from a hacker's point of view. Two important points are in the application used in this model: the point where a request is sent and the second one is a point received a response from the server. The snippet of the original Java method for performing request-response actions: Java boolean authenticate(String requestURL, String params) { URL url = null; HttpURLConnection conn = null; try { url = new URL(requestURL); conn = (HttpURLConnection) url.openConnection(); conn.connect(); } catch (IOException e) { showError("Failed connect to " + requestURL + "."); return false; // Warning } String response = ""; if (conn != null) { try (OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream())) { writer.write(params); // Warning writer.flush(); // Warning writer.close(); } catch (IOException e) { showError("Failed write params " + params + "."); return false; // Warning } try (BufferedReader reader = new BufferedReader(new InputStreamReader( conn.getInputStream()))) { String line = ""; while ((line = reader.readLine()) != null) { response += line + "\n"; } reader.close(); } catch (IOException e) { showError("Failed read " + response + "."); return false; // Warning } if (conn != null) conn.disconnect(); } return response.indexOf("activated") > -1; // Error } Vulnerable commands are followed by comments. The same Java method after obfuscation as a hacker sees it: Java static boolean Z(String a, String aa) { String var2 = a; HttpURLConnection var4 = null; try { (var4 = (HttpURLConnection)(new URL(var2)).openConnection()).connect(); } catch (IOException var36) { return true; /* false; */ } String var6 = ""; if (var4 != null) { Object var5; Throwable var10000; try { a = null; var5 = null; try { OutputStreamWriter var3 = new OutputStreamWriter(var4.getOutputStream()); try { // var3.write(a); // var3.flush(); var3.close(); } finally { if (var3 != null) { var3.close(); } } } catch (Throwable var38) { if (a == null) { var10000 = var38; } else { if (a != var38) { a.addSuppressed(var38); } var10000 = a; } // throw var10000; } } catch (IOException var39) { z("Failed connect to " + a + "."); return true; /* false; */ } HttpURLConnection var45; label534: { try { a = null; var5 = null; try { BufferedReader var43 = new BufferedReader(new InputStreamReader( var4.getInputStream())); boolean var21 = false; try { var21 = true; a = ""; BufferedReader var44 = var43; while(true) { if ((a = var44.readLine()) == null) { var43.close(); var21 = false; break; } var6 = var6 + a + "\n"; var44 = var43; } } finally { if (var21) { if (var43 != null) { var43.close(); } } } if (var43 != null) { var45 = var4; var43.close(); break label534; } } catch (Throwable var41) { if (a == null) { var10000 = var41; } else { if (a != var41) { a.addSuppressed(var41); } var10000 = a; } // throw var10000; } } catch (IOException var42) { z("Failed to read " + var2 + "."); return true; /* false; */ } var45 = var4; } if (var45 != null) { var4.disconnect(); } } return true; /* var6.indexOf(z("0'%-'%%!5")) > -1; */ } It's easy to see that by replacing line 97, as well as all previous return...s with return true, and comment lines 19 and 20 we get a free version of our program. Note that the server's attempts to determine who is currently applying for authentication and how many such requests will be unsuccessful (this is the case of a license for several persons). In obfuscated code, finding the output of a method can be much more difficult, but with the pathfinder method, or better like dogs find prey, it can be found sooner or later. The hacker starts from the first definition of the HttpURLConnection or similar class variable and follows the trail leading to the critical point: var4 -> var4 -> ... var3 -> var3 ... -> var4 -> return ... the rest of the garbage introduced by the obfuscator can be ignored. In this case, as in many others, the security of your program depends only on the ingenuity of the hacker and not on your efforts. We'll talk about this in the next part. Case 2: Pay and Play Below is the Pay and Play diagram showing corresponding components and data flows. This model has no trial. First, you pay, and then you get it. A hacker has nothing to hack. The following sentences have become a common rule for developers: “With enough time and effort, almost any code can be reverse-engineered. Obfuscators can make reverse engineering more difficult and economically impractical.” But this is a mistaken opinion. There is a so-called “Stole Once and Sold a Lot” attack. A hacker hacks a program not for himself but to sell it. For example, you can still buy illegal copies of MS Office, Windows 7 or 10, and many other software online for next to nothing. Thus, the hacker needs to buy the program and perform the steps described earlier for the Client-Server scheme: replace the corresponding line accepting the Activation Key with return true and delete the command in the payment order. To repel this simplest attack, the developer encrypts part of the code using any Java cryptographic algorithm with a key received from the server (Activation Key). The encrypted part includes key authentication, and launching the main program if successful. Below are the code snippets, including the code to decrypt and load the class. This code is used here as well as in the next part, Authenticator. Java public class Authenticator { public Authenticator(byte[] key, long ... l) { SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); IvParameterSpec iv = new IvParameterSpec(key); try { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, keySpec, iv); CipherInputStream cis = new CipherInputStream(getClass() .getResourceAsStream("anyfolder/anyname"), cipher); ByteArrayOutputStream baos = new ByteArrayOutputStream(); copyStream(cis, baos); byte[] bytes = baos.toByteArray(); Class<?> klass = new ClassLoader() { Class<?> define(byte[] bytes, int offset, int length) { Class<?> klass = super.defineClass(null, bytes, 0, length); return klass; } }.define(bytes, 0, bytes.length); klass.getConstructor().newInstance(); } catch (Throwable t) { System.exit(1); } } } Also, Launcher, which should be encrypted with the same Activation Key and placed into any resource folder. Java public class Launcher { args = Preloader.args; public Launcher() { Application.run(args); } } Where enclosing Preloader class which sets args values and the Activation Key received from the Server after payment confirmation. Java public class Preloader { private static byte[] key; public static String[] args; public static void main(String[] args) { receiveConfirmation(); encryptClass(); new Authenticator(key); } private static void receiveConfirmation() { String confirm = responce(); String[] parts = confirm.split(":"); key = hexToBytes(parts[0]); } private static void encryptClass() { IvParameterSpec iv = new IvParameterSpec(key); SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); try { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); InputStream is = Preloader.class.getResourceAsStream("/Launcher.class"); CipherInputStream cis = new CipherInputStream(is, cipher); String file = "C:\\Workspaces\\anyfolder\\anyname"; File targetFile = new File(file); OutputStream outStream = new FileOutputStream(targetFile); copyStream(cis, outStream); cis.close(); outStream.close(); } catch (Throwable t) { System.exit(1); } } private static byte[] hexToBytes(String hex) { byte[] bytes = new byte[hex.length() / 2]; for (int i = 0; i < bytes.length; i++) { try { bytes[i] = (byte) (0xff & Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16)); } catch (Exception e) { return null; } } return bytes; } private static void copyStream(InputStream in, OutputStream out) { byte[] buffer = new byte[4096]; int read; try { while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } } catch (IOException e) { System.exit(1); } } private static void sendPaymentOrder(float sum) { ... } private static String responce() { ... } } The advantages of this method of protection are obvious: An absence of logical expressions and variables that could be modified The pathfinder trace ends at the Authenticator class, which cannot be spoofed or bypassed There is no entry point to the main program, nor even its name We leave aside the issues of copy protection. Renaming variables, files, and classes do not affect the security level but may create an impression. Case 3: Time Bomb This diagram shows the parts and data flows of the Time Bomb model. This model is for time-limited trials and subscriptions. Here the counterclockwise timer acts as a server. It is located inside the program since placing it on the server will return us to the previous scheme. The request is "How much time is left" and the response is time > 0 ? run : exit. The critical point here is the counter itself and the output of the counter, similar to this: request->call counter->response->switch expired/no->. Static fields start and period are added to Launcher class and the constructor is a little bit modified. Java public class Launcher { private static long start; private static long period; private static String[] args; public class Application { public static void main(String[] args) { ... } } static { args = Preloader.args; start = Preloader.start; period = Preloader.period; } public Launcher() { if (System.currentTimeMillis() - start < period) { Application.main(args); } else { System.exit(1); } } } Unlike Case 1, a hacker cannot find the Launcher class using Java keywords (methods) related to the time and the line or lines to be changed. Therefore, this class must be reliably hidden from a hacker. Today encryption is the most suitable means for this. We do the following. Firstly, we encrypt Launcher.class bytes, secondly, we move them into a folder /anyfolder, rename the class to anyname, and then decrypt it using the same key with the Authenticator class, as in Case 2. Java public class Authenticator { public Authenticator(byte[] key, long ... l) { ... try { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); ... klass.getConstructor().newInstance(); } catch (Throwable t) { System.exit(1); } } } Preloader and Launcher classes are modifications of the Pay and Play model classes; parameters have been added for the Start and Time of work, start and period, respectively, as well as sum. Java public class Preloader { private static float sum = 1000000.00f; // added for Case 3 private static byte[] key; public static String[] args; public static long start; // added for Case 3 public static long period; // added for Case 3 public static void main(String[] args) { sendPaymentOrder(sum); // added for Case 3 receiveConfirmation(); encryptClass(); new Authenticator(key, start, period); } private static void receiveConfirmation() { String confirm = responce(); String[] parts = confirm.split(":"); key = hexToBytes(parts[0]); start = Long.parseLong(parts[1]); // added for Case 3 period = Long.parseLong(parts[2]); // added for Case 3 } private static void encryptClass() { IvParameterSpec iv = new IvParameterSpec(key); SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); try { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); InputStream is = Preloader.class.getResourceAsStream("/Launcher.class"); CipherInputStream cis = new CipherInputStream(is, cipher); String file = "C:\\Workspaces\\anyfolder\\anyname"; File targetFile = new File(file); OutputStream outStream = new FileOutputStream(targetFile); copyStream(cis, outStream); cis.close(); outStream.close(); } catch (Throwable t) { System.exit(1); } } private static byte[] hexToBytes(String hex) { ... return bytes; } private static void copyStream(InputStream in, OutputStream out) { ... } private static void sendPaymentOrder(float sum) { ... } } But all attempts to hide the Activation Key by encryption are frustrated by the fact that both the key and the classes are decrypted in the JVM memory. The hacker can use a memory dump to get the data he needs. First, it uses the Java Tools API. Code below: Java public class DumperAgent implements ClassFileTransformer { public static void premain(String args, Instrumentation instr) { agentmain(args, instr); } public static void agentmain(String agentArgs, Instrumentation instr) { instr.addTransformer(new DumperAgent(), true); Class<?>[] classes = instr.getAllLoadedClasses(); try { instr.retransformClasses(classes); } catch (UnmodifiableClassException e) {} } public byte[] transform(ClassLoader loader, String className, Class<?> redefinedClass, ProtectionDomain protDomain, byte[] classBytes) { dumpClass(className, classBytes); return null; } private static void dumpClass(String className, byte[] classBytes) { try { className = className.replace("/", File.separator); // ... FileOutputStream fos = new FileOutputStream(fileName); fos.write(classBytes); fos.close(); } catch (Exception e) { e.printStackTrace(); } } } And Attacher: Java public class Attacher { private static String pathToAttacherJar, pid; public static void main(String[] args) throws Exception { VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(pathToAttacherJar, null); } } Unfortunately, this attempt was also unsuccessful. A simple analysis of the Java run time arguments detects unwanted arguments, and the program stops working upon initialization without having time to load anything. Java static { RuntimeMXBean mxBean = ManagementFactory.getRuntimeMXBean(); if (mxBean.getInputArguments().contains("-XX:-DisableAttachMechanism") || mxBean.getInputArguments().contains("-javaagent:")) { System.exit(1); } } This fragment should be placed in the first class, which usually contains the main(String[] args) method. What Can You Do? The first thing you need to do is learn to see through the eyes of a hacker. This is identifying the architecture that your program is part of, looking for existing vulnerabilities, as well as possible vulnerabilities in your program and how to hack them. Remember that you can always find protection from any attack. Remember also that there is a “Steal Once, Sell Many” attack, but there is also a “Protect Well, Protect for Long” defense. More hackers pose as "distributors" of cracked software than you might think. Don't rely solely on obfuscators, servers, or cryptography. Protect only critical points and parts of programs. Use the structure of the program itself as it looks from the outside. Security should be as well designed as your program itself. Afterword Code protection is a process, not a final result. New hacking methods are being invented, new versions of the JVM are being released, allowing more manipulation of the JVM memory, etc. This is the same war as between viruses and anti-virus. In other words, neither absolute weapons nor absolute protection exist and cannot exist. Any new method of attack gives rise to a corresponding method of defense that repels this attack. An intrusion detection process similar to the observer effect in quantum physics should be used in almost every case of security algorithm development. Any observation (intervention), reset, remote agent, etc., causes a disturbance in the observed environment, which can be noticed and protective measures taken. Thus, we have a classic example of the evolution of the attack-defense system. This means that for every offensive action, there is a defense, and vice versa. Note that the defense is always in the best position.
As we venture into building on-device models, we're on the cusp of a surge in GenAI-related APIs and SaaS products. This shift resembles the past decade's trend towards backend and cloud services, and it behooves us to learn from that experience. I'm revisiting my earlier discussion on how to tailor SaaS APIs for mobile developers, using mobile developer personas for illustration. A UX (User Experience) persona is essentially a fictional character embodying a particular user group that might interact with a product, service, or website in a consistent manner. These personas are not just random creations; they are informed by specific demographic backgrounds and are characterized by aligned goals, motivations, and behavior patterns. This method serves as an effective strategy to structure, prioritize, and direct product experiences intuitively. When it comes to persona development, there are countless factors to consider. For the purpose of this discussion, I will deliberately sidestep elements such as “company size," and how it influences these personas. This omission is intended as a prompt for you, the reader, to explore further. It's also worth noting that these personas often result in divergent, conflicting developer experiences - a reality that must be navigated and reconciled in practical settings. Let's get started! Meet Harini, the Hacker Harini thrives in the Android ecosystem, though she's not as familiar with iOS. Her approach is pragmatic and efficient – she's all about getting things done swiftly, often opting for quick fixes to urgent problems. In critical situations, Harini is the go-to person, always ready to jump in and resolve crises. Her tech-savviness extends beyond her professional work, making her adept at optimizing her productivity (think keyboard shortcut aficionado). Harini's needs from SaaS APIs are clear: Stay current with the ecosystem: APIs should keep pace with the latest technological advancements, incorporating changes in operating systems, dependencies, and build tools. They must support the newest dependencies and offer multiple options for distributing software (npm, Maven, CocoaPods, Carthage, binary downloads, GitHub source, etc). Embrace open source: Harini prefers products that are open source, as it allows her to directly engage in bug reporting and fixing, an activity she finds rewarding. Simplified onboarding: The APIs should prioritize ease of onboarding, with a focus on usability over complex configurations. Community support: Having access to community forums, like those on Slack or Discord, where she can quickly get responses to her queries, is essential for Harini to get the job done. Automation-friendly APIs: Finally, a control plane API layer that facilitates the automation of administrative tasks is key for Harini to enhance her productivity. SaaS offerings that should target her use cases include: Products targeting small teams: Harini is an embodiment of someone who works in a small team going from zero to one. Offerings that help quickly prototype ideas best suited for zero-to-one teams that operate with high agency and no titles must absolutely target her needs. Strategically open-source focused products: Harini's knack for swiftly addressing urgent issues, coupled with her preference for open-source solutions, positions her as a potential influencer in developer communities. Her proactive involvement in bug reporting and solution implementation makes her an invaluable asset in enhancing these tools. Developer productivity tools: SaaS offerings that boost individual developer productivity, such as crash reporting tools, developer environments, and development platforms, align perfectly with Harini's role as a problem solver and crisis manager. Her influence on team dynamics and workflow efficiency makes her an ideal user for products that streamline development processes. SaaS offerings that may not target her use cases include: Highly specialized or niche tools: Products that focus on very specific platforms, operating systems, or programming languages may not resonate with Harini. Her generalist and pragmatic approach is less suited for tools that cater to narrowly defined technical niches. Configuration-intensive platforms: Platforms that demand extensive initial setup or in-depth onboarding processes are not in Harini's wheelhouse. Her preference for quick start-up and usability takes precedence, making her less likely to engage with such products. Compliance-driven platforms: SaaS solutions that are heavily oriented towards compliance and complex setups might not appeal to Harini. Her inclination towards straightforward, user-friendly tools means she is unlikely to favor platforms where compliance and extensive configuration overshadow functionality and ease of use. Meet Taylor, the Tech Lead Taylor is a tech lead who has a solid understanding of both Apple (including iOS, macOS, WatchOS, tvOS) and Android operating systems, though she's not an expert in either. She is well-acquainted with all the projects her team is working on and maintains oversight over various independent projects. Her focus is on ensuring software quality and stability, preferring to delay release if it means a better product. Taylor is proactive in troubleshooting her team's issues and regularly updates internal documentation. She's also keenly aware of how the product impacts business metrics. Taylor's expectations from SaaS APIs are comprehensive: Flexibility: APIs should be versatile in how they are used, and compatible with various build tools, programming languages, and frameworks. Taylor values having options and is cautious about being locked into a specific technology. Reliable support: She expects high-quality, preferably paid, support channels for assistance. Comprehensive documentation: Detailed release notes and extensive documentation for APIs are crucial for her to understand and use them effectively. Stability and reliability: The APIs should be stable, with a strong focus on fixing bugs and preventing crashes. Multi-environment support: The APIs should be usable across different stages of development, including the customer's development, staging, and production environments. Integrated analytics: Providing telemetry and integrating with analytics and observability APIs is important, as it helps in aligning tech with business metrics. Minimal breaking changes: Reducing the frequency of significant changes in APIs is essential to minimize the cost and effort of migration for her team. Data transparency: Clear communication about the data collected by the APIs and its implications, especially in different geographical locations, is necessary for compliance and trust. Examples of SaaS offerings that should target her use cases include: Products targeting mid to large teams: For products aimed at medium to large teams, Taylor's profile is incredibly relevant. With her focus on managing both deliverables and team development, her priorities are deeply rooted in the longevity and well-being of her products and team. Thus, offerings designed for larger teams should be attuned to her requirements. Examples include collaboration tools that facilitate effective teamwork and highly scalable cloud services that can support the growing demands of a larger team environment. Taylor's perspective is crucial in ensuring these tools not only meet technical requirements but also contribute positively to team dynamics and project sustainability. Flexible generalist tools: If your SaaS product leans towards flexibility rather than a rigid, opinionated structure due to the nature of the problem it solves, Taylor should be your target audience. Her broad expertise across platforms and her appreciation for versatile solutions make her an ideal user for these generalist tools. SaaS with support being a differentiator: Taylor places high value on reliable and responsive support. SaaS offerings that distinguish themselves with robust, possibly premium, technical support channels are well-aligned with her needs. Services known for their prompt assistance and consistent maintenance updates would resonate strongly with her. Quality-oriented products: Products whose development philosophy prioritizes quality over speed are perfect for Taylor. Her focus on ensuring software stability and her willingness to delay releases for better outcomes make her the perfect advocate for SaaS products that share this ethos. Security-first solutions: Products that prioritize security from the ground up will appeal to Taylor. As a tech lead responsible for overseeing various projects, she understands the importance of safeguarding data and ensuring secure operations, making her a fitting user for security-centric SaaS offerings. API first products: Taylor would appreciate API-centric products that adhere to Semantic Versioning (SemVer) principles and are willing to accept slower iteration cycles for the sake of maintaining stability and minimizing breaking changes. Her emphasis on minimal disruptions and stable integrations makes her a prime candidate for such products. Examples of SaaS offerings that may not target her use cases include: Rapid development and deployment tools: Solutions that emphasize speed and agility in development and deployment over quality and thorough testing might not resonate with Taylor. Harini may be the right persona for them (e.g.: document DBs that are expensive at scale). Products priced competitively at the cost of customer support: SaaS offerings that only provide basic or entry-level support might not meet Taylor’s expectations for high-quality, robust support channels. Her need for advanced, possibly paid support services makes her less likely to engage with platforms offering minimal support. Tools where compliance is not the focus: SaaS products that handle a large amount of data but lack a strong emphasis on data transparency and compliance might not appeal to Taylor. Given her concern for data implications, especially in different geographical locations, tools that do not prioritize these aspects may not be suitable for her. Meet Navya, the Newbie Navya is a budding software developer with less than a year of professional experience. Independent and eager to learn, she prefers figuring things out on her own rather than seeking help from more experienced colleagues like Taylor. Her passion for technology drives her to continuously enhance her knowledge and skills. Navya also harbors ambitions of starting her own tech startup in the future. To support her development journey, Navya looks for specific features in SaaS APIs: Clear examples and demos: She values APIs that provide straightforward examples and quickstart projects, helping her understand how to use them effectively. Understandable error logging: APIs that log clear and concise error messages make her learning process smoother and less intimidating. Community support: She appreciates when companies maintain and actively contribute to Stack Overflow, especially for common integration errors, as it helps her and others learn from these platforms. Educational resources: Access to structured learning through platforms like Coursera is essential for Navya to build her skills systematically. User-friendly interfaces: Starting with "drag and drop" interfaces before moving to more complex programmatic configurations helps Navya gradually build her technical prowess in a manageable way. Examples of SaaS offerings that should target her use cases include: Strategic loss leader platforms: These are offerings that target students and early-career developers with the long-term strategy of gaining mindshare, which could lead to future benefits when these individuals influence their workplaces' tool selections. For example, integrated development environments (IDEs) tailored for novice developers can be a perfect fit for Navya. By providing accessible and beginner-friendly features, these platforms can foster loyalty and familiarity, which Navya might later bring into her professional environment. Collaboration-first platforms: Given Navya's preference for pairing and seeking help from peers, platforms that emphasize collaborative functionalities would be highly suitable. These could include tools that allow for real-time code sharing, joint problem-solving, or peer review mechanisms. By facilitating an environment where Navya can easily collaborate and learn from others, these platforms can significantly enhance her development experience and skills. Examples of SaaS offerings that may not target her use cases include: Offerings that lack a free tier: Expensive enterprise-level SaaS products, which are typically designed for large-scale business environments and come with a hefty price tag, are not ideal for Navya. As a beginner with potential budget constraints, she is unlikely to benefit from or afford these advanced solutions. Tools with steep learning curves: Software that requires a steep learning curve, with little guidance or support for beginners, might be overwhelming for Navya. These tools could hinder rather than help her learning process due to their complexity and lack of beginner-friendly features. Conclusion In conclusion, understanding the diverse personas of mobile developers like Harini, Taylor, and Navya is crucial for creating Gen AI products that effectively meet their distinct needs. Harini, the pragmatic hacker, seeks cutting-edge, open-source solutions and efficient problem-solving tools. Taylor, the experienced tech lead, requires stability, comprehensive documentation, and versatile API usage. Navya, the enthusiastic newcomer, looks for clear guidance, educational resources, and user-friendly interfaces. By focusing on these personas and sometimes picking one over the other, developers and companies can build more effective products that empower a wide range of professionals as we embark on the GenAI SaaS revolution.
As organizations increasingly migrate their applications to the cloud, efficient and scalable load balancing becomes pivotal for ensuring optimal performance and high availability. This article provides an overview of Azure's load balancing options, encompassing Azure Load Balancer, Azure Application Gateway, Azure Front Door Service, and Azure Traffic Manager. Each of these services addresses specific use cases, offering diverse functionalities to meet the demands of modern applications. Understanding the strengths and applications of these load-balancing services is crucial for architects and administrators seeking to design resilient and responsive solutions in the Azure cloud environment. What Is Load Balancing? Load balancing is a critical component in cloud architectures for various reasons. Firstly, it ensures optimized resource utilization by evenly distributing workloads across multiple servers or resources, preventing any single server from becoming a performance bottleneck. Secondly, load balancing facilitates scalability in cloud environments, allowing resources to be scaled based on demand by evenly distributing incoming traffic among available resources. Additionally, load balancers enhance high availability and reliability by redirecting traffic to healthy servers in the event of a server failure, minimizing downtime, and ensuring accessibility. From a security perspective, load balancers implement features like SSL termination, protecting backend servers from direct exposure to the internet, and aiding in mitigating DDoS attacks and threat detection/protection using Web Application Firewalls. Furthermore, efficient load balancing promotes cost efficiency by optimizing resource allocation, preventing the need for excessive server capacity during peak loads. Finally, dynamic traffic management across regions or geographic locations capabilities allows load balancers to adapt to changing traffic patterns, intelligently distributing traffic during high-demand periods and scaling down resources during low-demand periods, leading to overall cost savings. Overview of Azure’s Load Balancing Options Azure Load Balancer: Unleashing Layer 4 Power Azure Load Balancer is a Layer 4 (TCP, UDP) load balancer that distributes incoming network traffic across multiple Virtual Machines or Virtual Machine Scalesets to ensure no single server is overwhelmed with too much traffic. There are 2 options for the load balancer: a Public Load Balancer primarily used for internet traffic and also supports outbound connection, and a Private Load Balancer to load balance traffic with a virtual network. The load balancer uses a five-tuple (source IP, source port, destination IP, destination port, protocol). Features High availability and redundancy: Azure Load Balancer efficiently distributes incoming traffic across multiple virtual machines or instances in a web application deployment, ensuring high availability, redundancy, and even distribution, thereby preventing any single server from becoming a bottleneck. In the event of a server failure, the load balancer redirects traffic to healthy servers. Provide outbound connectivity: The frontend IPs of a public load balancer can be used to provide outbound connectivity to the internet for backend servers and VMs. This configuration uses source network address translation (SNAT) to translate the virtual machine's private IP into the load balancer's public IP address, thus preventing outside sources from having a direct address to the backend instances. Internal load balancing: Distribute traffic across internal servers within a Virtual Network (VNet); this ensures that services receive an optimal share of resources Cross-region load balancing: Azure Load Balancer facilitates the distribution of traffic among virtual machines deployed in different Azure regions, optimizing performance and ensuring low-latency access for users of global applications or services with a user base spanning multiple geographic regions. Health probing and failover: Azure Load Balancer monitors the health of backend instances continuously, automatically redirecting traffic away from unhealthy instances, such as those experiencing application errors or server failures, to ensure seamless failover. Port-level load balancing: For services running on different ports within the same server, Azure Load Balancer can distribute traffic based on the specified port numbers. This is useful for applications with multiple services running on the same set of servers. Multiple front ends: Azure Load Balancer allows you to load balance services on multiple ports, multiple IP addresses, or both. You can use a public or internal load balancer to load balance traffic across a set of services like virtual machine scale sets or virtual machines (VMs). High Availability (HA) ports in Azure Load Balancer play a crucial role in ensuring resilient and reliable network traffic management. These ports are designed to enhance the availability and redundancy of applications by providing failover capabilities and optimal performance. Azure Load Balancer achieves this by distributing incoming network traffic across multiple virtual machines to prevent a single point of failure. Configuration and Optimization Strategies Define a well-organized backend pool, incorporating healthy and properly configured virtual machines (VMs) or instances, and consider leveraging availability sets or availability zones to enhance fault tolerance and availability. Define load balancing rules to specify how incoming traffic should be distributed. Consider factors such as protocol, port, and backend pool association. Use session persistence settings when necessary to ensure that requests from the same client are directed to the same backend instance. Configure health probes to regularly check the status of backend instances. Adjust probe settings, such as probing intervals and thresholds, based on the application's characteristics. Choose between the Standard SKU and the Basic SKU based on the feature set required for your application. Implement frontend IP configurations to define how the load balancer should handle incoming network traffic. Implement Azure Monitor to collect and analyze telemetry data, set up alerts based on performance thresholds for proactive issue resolution, and enable diagnostics logging to capture detailed information about the load balancer's operations. Adjust the idle timeout settings to optimize the connection timeout for your application. This is especially important for applications with long-lived connections. Enable accelerated networking on virtual machines to take advantage of high-performance networking features, which can enhance the overall efficiency of the load-balanced application. Azure Application Gateway: Elevating To Layer 7 Azure Application Gateway is a Layer 7 load balancer that provides advanced traffic distribution and web application firewall (WAF) capabilities for web applications. Features Web application routing: Azure Application Gateway allows for the routing of requests to different backend pools based on specific URL paths or host headers. This is beneficial for hosting multiple applications on the same set of servers. SSL termination and offloading: Improve the performance of backend servers by transferring the resource-intensive task of SSL decryption to the Application Gateway and relieving backend servers of the decryption workload. Session affinity: For applications that rely on session state, Azure Application Gateway supports session affinity, ensuring that subsequent requests from a client are directed to the same backend server for a consistent user experience. Web Application Firewall (WAF): Implement a robust security layer by integrating the Azure Web Application Firewall with the Application Gateway. This helps safeguard applications from threats such as SQL injection, cross-site scripting (XSS), and other OWASP Top Ten vulnerabilities. You can define your own WAF custom firewall rules as well. Auto-scaling: Application Gateway can automatically scale the number of instances to handle increased traffic and scale down during periods of lower demand, optimizing resource utilization. Rewriting HTTP headers: Modify HTTP headers for requests and responses, as adjusting these headers is essential for reasons including adding security measures, altering caching behavior, or tailoring responses to meet client-specific requirements. Ingress Controller for AKS: The Application Gateway Ingress Controller (AGIC) enables the utilization of Application Gateway as the ingress for an Azure Kubernetes Service (AKS) cluster. WebSocket and HTTP/2 traffic: Application Gateway provides native support for the WebSocket and HTTP/2 protocols. Connection draining: This pivotal feature ensures the smooth and graceful removal of backend pool members during planned service updates or instances of backend health issues. This functionality promotes seamless operations and mitigates potential disruptions by allowing the system to handle ongoing connections gracefully, maintaining optimal performance and user experience during transitional periods Configuration and Optimization Strategies Deploy the instances in a zone-aware configuration, where available. Use Application Gateway with Web Application Firewall (WAF) within a virtual network to protect inbound HTTP/S traffic from the Internet. Review the impact of the interval and threshold settings on health probes. Setting a higher interval puts a higher load on your service. Each Application Gateway instance sends its own health probes, so 100 instances every 30 seconds means 100 requests per 30 seconds. Use App Gateway for TLS termination. This promotes the utilization of backend servers because they don't have to perform TLS processing and easier certificate management because the certificate only needs to be installed on Application Gateway. When WAF is enabled, every request gets buffered until it fully arrives, and then it gets validated against the ruleset. For large file uploads or large requests, this can result in significant latency. The recommendation is to enable WAF with proper testing and validation. Having appropriate DNS and certificate management for backend pools is crucial for improved performance. Application Gateway does not get billed in stopped state. Turn it off for the dev/test environments. Take advantage of features for autoscaling and performance benefits, and make sure to have scale-in and scale-out instances based on the workload to reduce the cost. Use Azure Monitor Network Insights to get a comprehensive view of health and metrics, crucial in troubleshooting issues. Azure Front Door Service: Global-Scale Entry Management Azure Front Door is a comprehensive content delivery network (CDN) and global application accelerator service that provides a range of use cases to enhance the performance, security, and availability of web applications. Azure Front Door supports four different traffic routing methods latency, priority, weighted, and session affinity to determine how your HTTP/HTTPS traffic is distributed between different origins. Features Global content delivery and acceleration: Azure Front Door leverages a global network of edge locations, employing caching mechanisms, compressing data, and utilizing smart routing algorithms to deliver content closer to end-users, thereby reducing latency and enhancing overall responsiveness for an improved user experience. Web Application Firewall (WAF): Azure Front Door integrates with Azure Web Application Firewall, providing a robust security layer to safeguard applications from common web vulnerabilities, such as SQL injection and cross-site scripting (XSS). Geo filtering: In Azure Front Door WAF you can define a policy by using custom access rules for a specific path on your endpoint to allow or block access from specified countries or regions. Caching: In Azure Front Door, caching plays a pivotal role in optimizing content delivery and enhancing overall performance. By strategically storing frequently requested content closer to the end-users at the edge locations, Azure Front Door reduces latency, accelerates the delivery of web applications, and prompts resource conservation across entire content delivery networks. Web application routing: Azure Front Door supports path-based routing, URL redirect/rewrite, and rule sets. These help to intelligently direct user requests to the most suitable backend based on various factors such as geographic location, health of backend servers, and application-defined routing rules. Custom domain and SSL support: Front Door supports custom domain configurations, allowing organizations to use their own domain names and SSL certificates for secure and branded application access. Configuration and Optimization Strategies Use WAF policies to provide global protection across Azure regions for inbound HTTP/S connections to a landing zone. Create a rule to block access to the health endpoint from the internet. Ensure that the connection to the back end is re-encrypted as Front Door does support SSL passthrough. Consider using geo-filtering in Azure Front Door. Avoid combining Traffic Manager and Front Door as they are used for different use cases. Configure logs and metrics in Azure Front Door and enable WAF logs for debugging issues. Leverage managed TLS certificates to streamline the costs and renewal process associated with certificates. Azure Front Door service issues and rotates these managed certificates, ensuring a seamless and automated approach to certificate management, thereby enhancing security while minimizing operational overhead. Use the same domain name on Front Door and your origin to avoid any issues related to request cookies or URL redirections. Disable health probes when there’s only one origin in an origin group. It's recommended to monitor a webpage or location that you specifically designed for health monitoring. Regularly monitor and adjust the instance count and scaling settings to align with actual demand, preventing overprovisioning and optimizing costs. Azure Traffic Manager: DNS-Based Traffic Distribution Azure Traffic Manager is a global DNS-based traffic load balancer that enhances the availability and performance of applications by directing user traffic to the most optimal endpoint. Features Global load balancing: Distribute user traffic across multiple global endpoints to enhance application responsiveness and fault tolerance. Fault tolerance and high availability: Ensure continuous availability of applications by automatically rerouting traffic to healthy endpoints in the event of failures. Routing: Support various routing globally. Performance-based routing optimizes application responsiveness by directing traffic to the endpoint with the lowest latency. Geographic traffic routing is based on the geographic location of end-users, priority-based, weighted, etc. Endpoint monitoring: Regularly check the health of endpoints using configurable health probes, ensuring traffic is directed only to operational and healthy endpoints. Service maintenance: You can have planned maintenance done on your applications without downtime. Traffic Manager can direct traffic to alternative endpoints while the maintenance is in progress. Subnet traffic routing: Define custom routing policies based on IP address ranges, providing flexibility in directing traffic according to specific network configurations. Configuration and Optimization Strategies Enable automatic failover to healthy endpoints in case of endpoint failures, ensuring continuous availability and minimizing disruptions. Utilize appropriate traffic routing methods, such as Priority, Weighted, Performance, Geographic, and Multi-value, to tailor traffic distribution based on specific application requirements. Implement a custom page to use as a health check for your Traffic Manager. If the Time to Live (TTL) interval of the DNS record is too long, consider adjusting the health probe timing or DNS record TTL. Consider nested Traffic Manager profiles. Nested profiles allow you to override the default Traffic Manager behavior to support larger, more complex application deployments. Integrate with Azure Monitor for real-time monitoring and logging, gaining insights into the performance and health of Traffic Manager and endpoints. How To Choose When selecting a load balancing option in Azure, it is crucial to first understand the specific requirements of your application, including whether it necessitates layer 4 or layer 7 load balancing, SSL termination, and web application firewall capabilities. For applications requiring global distribution, options like Azure Traffic Manager or Azure Front Door are worth considering to efficiently achieve global load balancing. Additionally, it's essential to evaluate the advanced features provided by each load balancing option, such as SSL termination, URL-based routing, and application acceleration. Scalability and performance considerations should also be taken into account, as different load balancing options may vary in terms of throughput, latency, and scaling capabilities. Cost is a key factor, and it's important to compare pricing models to align with budget constraints. Lastly, assess how well the chosen load balancing option integrates with other Azure services and tools within your overall application architecture. This comprehensive approach ensures that the selected load balancing solution aligns with the unique needs and constraints of your application. Service Global/Regional Recommended traffic Azure Front Door Global HTTP(S) Azure Traffic Manager Global Non-HTTP(S) and HTTPS Azure Application Gateway Regional HTTP(S) Azure Load Balancer Regional or Global Non-HTTP(S) and HTTPS Here is the decision tree for load balancing from Azure. Source: Azure
This tutorial illustrates B2B push-style application integration with APIs and internal integration with messages. We have the following use cases: Ad Hoc Requests for information (Sales, Accounting) that cannot be anticipated in advance. Two Transaction Sources: A) internal Order Entry UI, and B) B2B partner OrderB2B API. The Northwind API Logic Server provides APIs and logic for both transaction sources: Self-Serve APIs to support ad hoc integration and UI dev, providing security (e.g., customers see only their accounts). Order Logic: enforcing database integrity and Application Integration (alert shipping). A custom API to match an agreed-upon format for B2B partners. The Shipping API Logic Server listens to Kafka and processes the message. Key Architectural Requirements: Self-Serve APIs and Shared Logic This sample illustrates some key architectural considerations: Requirement Poor Practice Good Practice Best Practice Ideal Ad Hoc Integration ETL APIs Self-Serve APIs Automated Self-Serve APIs Logic Logic in UI Reusable Logic Declarative Rules.. Extensible with Python Messages Kafka Kafka Logic Integration We'll further expand on these topics as we build the system, but we note some best practices: APIs should be self-serve, not requiring continuing server development. APIs avoid the nightly Extract, Transfer, and Load (ETL) overhead. Logic should be re-used over the UI and API transaction sources. Logic in UI controls is undesirable since it cannot be shared with APIs and messages. Using This Guide This guide was developed with API Logic Server, which is open-source and available here. The guide shows the highlights of creating the system. The complete Tutorial in the Appendix contains detailed instructions to create the entire running system. The information here is abbreviated for clarity. Development Overview This overview shows all the key codes and procedures to create the system above. We'll be using API Logic Server, which consists of a CLI plus a set of runtimes for automating APIs, logic, messaging, and an admin UI. It's an open-source Python project with a standard pip install. 1. ApiLogicServer Create: Instant Project The CLI command below creates an ApiLogicProject by reading your schema. The database is Northwind (Customer, Orders, Items, and Product), as shown in the Appendix. Note: the db_urlvalue is an abbreviation; you normally supply a SQLAlchemy URL. The sample NW SQLite database is included in ApiLogicServer for demonstration purposes. $ ApiLogicServer create --project_name=ApiLogicProject --db_url=nw- The created project is executable; it can be opened in an IDE and executed. One command has created meaningful elements of our system: an API for ad hoc integration and an Admin App. Let's examine these below. API: Ad Hoc Integration The system creates a JSON API with endpoints for each table, providing filtering, sorting, pagination, optimistic locking, and related data access. JSON: APIs are self-serve: consumers can select their attributes and related data, eliminating reliance on custom API development. In this sample, our self-serve API meets our Ad Hoc Integration needs and unblocks Custom UI development. Admin App: Order Entry UI The create command also creates an Admin App: multi-page, multi-table with automatic joins, ready for business user agile collaboration and back office data maintenance. This complements custom UIs you can create with the API. Multi-page navigation controls enable users to explore data and relationships. For example, they might click the first Customer and see their Orders and Items: We created an executable project with one command that completes our ad hoc integration with a self-serve API. 2. Customize: In Your IDE While API/UI automation is a great start, we now require Custom APIs, Logic, and Security. Such customizations are added to your IDE, leveraging all its services for code completion, debugging, etc. Let's examine these. Declare UI Customizations The admin app is not built with complex HTML and JavaScript. Instead, it is configured with the ui/admin/admin.yml, automatically created from your data model by the ApiLogicServer create command. You can customize this file in your IDE to control which fields are shown (including joins), hide/show conditions, help text, etc. This makes it convenient to use the Admin App to enter an Order and OrderDetails: Note the automation for automatic joins (Product Name, not ProductId) and lookups (select from a list of Products to obtain the foreign key). If we attempt to order too much Chai, the transaction properly fails due to the Check Credit logic described below. Check Credit Logic: Multi-Table Derivation and Constraint Rules, 40X More Concise. Such logic (multi-table derivations and constraints) is a significant portion of a system, typically nearly half. API Logic server provides spreadsheet-like rules that dramatically simplify and accelerate logic development. The five check credit rules below represent the same logic as 200 lines of traditional procedural code. Rules are 40X more concise than traditional code, as shown here. Rules are declared in Python and simplified with IDE code completion. Rules can be debugged using standard logging and the debugger: Rules operate by handling SQLAlchemy events, so they apply to all ORM access, whether by the API engine or your custom code. Once declared, you don't need to remember to call them, which promotes quality. The above rules prevented the too-big order with multi-table logic from copying the Product Price, computing the Amount, rolling it up to the AmountTotal and Balance, and checking the credit. These five rules also govern changing orders, deleting them, picking different parts, and about nine automated transactions. Implementing all this by hand would otherwise require about 200 lines of code. Rules are a unique and significant innovation, providing meaningful improvements over procedural logic: CHARACTERISTIC PROCEDURAL DECLARATIVE WHY IT MATTERS Reuse Not Automatic Automatic - all Use Cases 40X Code Reduction Invocation Passive - only if called Active - call not required Quality Ordering Manual Automatic Agile Maintenance Optimizations Manual Automatic Agile Design For more on the rules, click here. Declare Security: Customers See Only Their Own Row Declare row-level security using your IDE to edit logic/declare_security.sh, (see screenshot below). An automatically created admin app enables you to configure roles, users, and user roles. If users now log in as ALFKI (configured with role customer), they see only their customer row. Observe the console log at the bottom shows how the filter worked. Declarative row-level security ensures users see only the rows authorized for their roles. 3. Integrate: B2B and Shipping We now have a running system, an API, logic, security, and a UI. Now, we must integrate with the following: B2B partners: We'll create a B2B Custom Resource. OrderShipping: We add logic to Send an OrderShipping Message. B2B Custom Resource The self-serve API does not conform to the format required for a B2B partnership. We need to create a custom resource. You can create custom resources by editing customize_api.py using standard Python, Flask, and SQLAlchemy. A custom OrderB2B endpoint is shown below. The main task here is to map a B2B payload onto our logic-enabled SQLAlchemy rows. API Logic Server provides a declarative RowDictMapper class you can use as follows: Declare the row/dict mapping; see the OrderB2B class in the lower pane: Note the support for lookup so that partners can send ProductNames, not ProductIds. Create the custom API endpoint; see the upper pane: Add def OrderB2B to customize_api/py to create a new endpoint. Use the OrderB2B class to transform API request data to SQLAlchemy rows (dict_to_row). The automatic commit initiates the shared logic described above to check credit and reorder products. Our custom endpoint required under ten lines of code and the mapper configuration. Produce OrderShipping Message Successful orders must be sent to Shipping in a predesignated format. We could certainly POST an API, but Messaging (here, Kafka) provides significant advantages: Async: Our system will not be impacted if the Shipping system is down. Kafka will save the message and deliver it when Shipping is back up. Multi-cast: We can send a message that multiple systems (e.g., Accounting) can consume. The content of the message is a JSON string, just like an API. Just as you can customize APIs, you can complement rule-based logic using Python events: Declare the mapping; see the OrderShipping class in the right pane. This formats our Kafka message content in the format agreed upon with Shipping. Define an after_flush event, which invokes send_order_to_shipping. This is called by the logic engine, which passes the SQLAlchemy models.Order row. send_order_to_shipping uses OrderShipping.row_to_dict to map our SQLAlchemy order row to a dict and uses the Kafka producer to publish the message. Rule-based logic is customizable with Python, producing a Kafka message with 20 lines of code here. 4. Consume Messages The Shipping system illustrates how to consume messages. The sections below show how to create/start the shipping server create/start and use our IDE to add the consuming logic. Create/Start the Shipping Server This shipping database was created from AI. To simplify matters, API Logic Server has installed the shipping database automatically. We can, therefore, create the project from this database and start it: 1. Create the Shipping Project ApiLogicServer create --project_name=shipping --db_url=shipping 2. Start your IDE (e.g., code shipping) and establish your venv. 3. Start the Shipping Server: F5 (configured to use a different port). The core Shipping system was automated by ChatGPT and ApiLogicServer create. We add 15 lines of code to consume Kafka messages, as shown below. Consuming Logic To consume messages, we enable message consumption, configure a mapping, and provide a message handler as follows. 1. Enable Consumption Shipping is pre-configured to enable message consumption with a setting in config.py: KAFKA_CONSUMER = '{"bootstrap.servers": "localhost:9092", "group.id": "als-default-group1", "auto.offset.reset":"smallest"}' When the server is started, it invokes flask_consumer() (shown below). This is called the pre-supplied FlaskKafka, which handles the Kafka consumption (listening), thread management, and the handle annotation used below. This housekeeping task is pre-created automatically. FlaskKafka was inspired by the work of Nimrod (Kevin) Maina in this project. Many thanks! 2. Configure a Mapping As we did for our OrderB2B Custom Resource, we configured an OrderToShip mapping class to map the message onto our SQLAlchemy Order object. 3. Provide a Consumer Message Handler We provide the order_shipping handler in kafka_consumer.py: Annotate the topic handler method, providing the topic name. This is used by FlaskKafka to establish a Kafka listener Provide the topic handler code, leveraging the mapper noted above. It is called FlaskKafka per the method annotations. Test It You can use your IDE terminal window to simulate a business partner posting a B2BOrder. You can set breakpoints in the code described above to explore system operation. ApiLogicServer curl "'POST' 'http://localhost:5656/api/ServicesEndPoint/OrderB2B'" --data ' {"meta": {"args": {"order": { "AccountId": "ALFKI", "Surname": "Buchanan", "Given": "Steven", "Items": [ { "ProductName": "Chai", "QuantityOrdered": 1 }, { "ProductName": "Chang", "QuantityOrdered": 2 } ] } }}' Use Shipping's Admin App to verify the Order was processed. Summary These applications have demonstrated several types of application integration: Ad Hoc integration via self-serve APIs. Custom integration via custom APIs to support business agreements with B2B partners. Message-based integration to decouple internal systems by reducing dependencies that all systems must always be running. We have also illustrated several technologies noted in the ideal column: Requirement Poor Practice Good Practice Best Practice Ideal Ad Hoc Integration ETL APIs Self-Serve APIs Automated Creation of Self-Serve APIs Logic Logic in UI Reusable Logic Declarative Rules.. Extensible with Python Messages Kafka Kafka Logic Integration API Logic Server provides automation for the ideal practices noted above: 1. Creation: instant ad hoc API (and Admin UI) with the ApiLogicServer create command. 2. Declarative Rules: Security and multi-table logic reduce the backend half of your system by 40X. 3. Kafka Logic Integration Produce messages from logic events. Consume messages by extending kafka_consumer. Services, including: RowDictMapper to transform rows and dict. FlaskKafka for Kafka consumption, threading, and annotation invocation. 4. Standards-based Customization Standard packages: Python, Flask, SQLAlchemy, Kafka... Using standard IDEs. Creation, logic, and integration automation have enabled us to build two non-trivial systems with a remarkably small amount of code: Type Code Custom B2B API 10 lines Check Credit Logic 5 rules Row Level Security 1 security declaration Send Order to Shipping 20 lines Process Order in Shipping 30 lines Mapping configurationsto transform rows and dicts 45 lines Automation dramatically increases time to market, with standards-based customization using your IDE, Python, Flask, SQLAlchemy, and Kafka. For more information on API Logic Server, click here. Appendix Full Tutorial You can recreate this system and explore running code, including Kafka, click here. It should take 30-60 minutes, depending on whether you already have Python and an IDE installed. Sample Database The sample database is an SQLite version of Northwind, Customers, Order, OrderDetail, and Product. To see a database diagram, click here. This database is included when you pip install ApiLogicServer.
As AI services and the data they consume and create become more important and prevalent in various applications and processes, so do the platforms and architectures they are built upon. As usual, there is no “one size fits all.” However, what is briefly presented here is an optimized approach to such data-driven AI application architectures. All of the source code mentioned and more can be found here and a free “Develop with Oracle AI and Database Services: Gen, Vision, Speech, Language, and OML” workshop (where all of the use cases are based on the U.N.’s 17 Sustainable Development Goals) giving many more examples can be found here. Often multiple network calls must be made in a given AI app, entailing calls to the AI services as well as calls to retrieve and persist the content (whether it be text, audio, images, video, etc.) that is the input or output. The persistent information is then often processed and analyzed further, and additional calls, AI or otherwise, are made in reaction. The Oracle Database provides the ability to make calls out to other services, again AI and otherwise, whether they be within the Oracle Cloud or external. When the calls are instead made from the database itself, it provides an optimized architecture with various benefits, including: Reduced network calls, thus reducing latency. Reduced network calls, thus increasing reliability. Transactional (ACID) operations on AI and other data (and even messaging when using TxEventQ) which avoid the need for idempotent/duplicate processing logic, etc., and the related wasted resources there. Processing optimization is due to the locality of data whether that data is stored directly in the database or an object store or other source. This is because the Oracle Database provides a robust functional frontend to otherwise dumb object storage buckets and the database provides many options to either sync or optimally operate on data in place in the object store and other data sources. Enhanced security due to a common authentication mechanism and reuse of a widely acknowledged robust database and cloud security infrastructure. Reduced overall configuration as calls are made from a central location. The entry point to the database itself can be exposed as a REST endpoint (using ORDS) with a single click, and of course, drivers in various languages can be used to access the database as well. Vector database advantages. This topic is an article unto itself, and one I’ll release as a follow-on, especially as Oracle has and is adding several features in this area. Oracle Database Machine Learning. In addition to various AI services, the Oracle Database itself has had a machine learning engine for many years. Oracle Autonomous Database Select AI, which enables querying data using natural language and generating SQL that is specific to your database. Oracle Autonomous Database AI Vector Search, which includes a new vector data type, vector indexes, and vector search SQL operators that enable the Oracle Database to "store the semantic content of documents, images, and other unstructured data as vectors, and use these to run fast similarity queries." These new capabilities also support RAG (Retrieval Augmented Generation), which provides higher accuracy and avoids having to expose private data by including it in the LLM training data. Again, there are many different AI application flows and requirements, but a basic comparison of the two approaches can be visualized in the following way: The Code It is possible to run several different languages in the database, making it possible to conduct various application logic there. These include Java, JavaScript, and PL/SQL. PL/SQL examples are given here, and they can be executed from the Database Actions -> SQL page in the OCI console, from the SQLcl command line tool (which is pre-installed in the OCI Cloud Shell or can be downloaded), from SQLDeveloper, VS Code (where Oracle has a plugin), etc. There are also a few ways to go about making the calls out to AI and other services. Standard REST calls using the database’s UTL_HTTP package or fetch from JavaScript, etc. is one approach. If the AI services run within OCI (the Oracle Cloud) then OCI SDKs, which are written for all major languages, can also be used. I find the use of the DBMS_CLOUD.send_request package for all OCI services calls (rather than, for example, more specific OCI SDK calls such as DBMS_CLOUD_OCI_AIV_AI_SERVICE_VISION) to be the simplest and most dynamic approach. We start by creating a credential that can be referenced and reused for all of our cloud services calls and simply includes the information from your OCI account/config. PLSQL BEGIN dbms_cloud.create_credential ( credential_name => 'OCI_KEY_CRED', user_ocid => 'ocid1.user.oc1..[youruserocid]', tenancy_ocid => 'ocid1.tenancy.oc1..[yourtenancyocid]', private_key => '[yourprivatekey - you can read this from file or put the contents of your pem without header, footer, and line wraps]' fingerprint => '[7f:yourfingerprint]' ); END; Next, before we look at the main program/function, let’s just quickly just look at the table we’ll save the AI results in. Notice, in this case, the table has columns for both the JSON from an AI call return and a text field that is created from key fields in the JSON for quick reference, searches, etc. Again, the table structures, use of SQL/relational versus JSON, etc. may all vary, and again, this is a great example of the Oracle multi-purpose database where you can use various data models and data types. For example, the JSON Duality feature in the Oracle Database may be worth checking out, as it allows the same data to be accessed using SQL/relational as well as JSON and even MongoDB APIs. PLSQL CREATE TABLE aivision_results (id RAW (16) NOT NULL, date_loaded TIMESTAMP WITH TIME ZONE, label varchar2(20), textfromai varchar2(32767), jsondata CLOB CONSTRAINT ensure_aivision_results_json CHECK (jsondata IS JSON)); / And now, the simple function that typifies the heart of the architecture… Here we see a call to DBMS_CLOUD.send_request with the credential we created and the URL of the (AI) service operation endpoint (the analyzeImage operation of the Oracle Vision AI service, in this case). The JSON payload of the body consists of the feature(s) of the service that we would like to use and any other config as well as the arguments to the operation which in this case include the object storage location of an image (another option would be to provide the image byte array directly/inlined as part of the payload). The JSON result is then retrieved from the response, certain elements of it are parsed out into a text field for convenience, and the JSON, text, etc. are persisted, as mentioned earlier. PLSQL CREATE OR REPLACE FUNCTION VISIONAI_TEXTDETECTION ( p_endpoint VARCHAR2, p_compartment_ocid VARCHAR2, p_namespaceName VARCHAR2, p_bucketName VARCHAR2, p_objectName VARCHAR2, p_featureType VARCHAR2, p_label VARCHAR2 ) RETURN VARCHAR2 IS resp DBMS_CLOUD_TYPES.resp; json_response CLOB; v_textfromai VARCHAR2(32767); BEGIN resp := DBMS_CLOUD.send_request( credential_name => 'OCI_KEY_CRED', uri => p_endpoint || '/20220125/actions/analyzeImage', method => 'POST', body => UTL_RAW.cast_to_raw( JSON_OBJECT( 'features' VALUE JSON_ARRAY( JSON_OBJECT('featureType' VALUE p_featureType) ), 'image' VALUE JSON_OBJECT( 'source' VALUE 'OBJECT_STORAGE', 'namespaceName' VALUE p_namespaceName, 'bucketName' VALUE p_bucketName, 'objectName' VALUE p_objectName ), 'compartmentId' VALUE p_compartment_ocid ) ) ); json_response := DBMS_CLOUD.get_response_text(resp); SELECT LISTAGG(text, ', ') WITHIN GROUP (ORDER BY ROWNUM) INTO v_textfromai FROM JSON_TABLE(json_response, '$.imageText.words[*]' COLUMNS ( text VARCHAR2(100) PATH '$.text' ) ); INSERT INTO aivision_results (id, date_loaded, label, textfromai, jsondata) VALUES (SYS_GUID(), SYSTIMESTAMP, p_label, v_textfromai, json_response); RETURN v_textfromai; EXCEPTION WHEN OTHERS THEN RAISE; END VISIONAI_TEXTDETECTION; / We can also expose the function as a REST endpoint programmatically using the following: PLSQL BEGIN ORDS.ENABLE_OBJECT( P_ENABLED => TRUE, P_SCHEMA => 'AIUSER', P_OBJECT => 'VISIONAI_OBJECTDETECTION', P_OBJECT_TYPE => 'FUNCTION', P_OBJECT_ALIAS => 'VISIONAI_OBJECTDETECTION', P_AUTO_REST_AUTH => FALSE ); COMMIT; END; / Analysis and Text Searching of AI Results This architecture also makes analysis and text searching of all AI results more efficient. From here, more processing and analytics can take place. Let's take a look at three statements that will provide us with an easy-to-use text search of our AI results. First, we create an index for text searches on our aivision_results table. Then we create a function that searches for a given string using the powerful contains functionality, or we could additionally/optionally use the DBMS_SEARCH package to search multiple tables, and return the refcursor of results. Finally, we expose the function as a REST endpoint. It's that simple. PLSQL create index aivisionresultsindex on aivision_results(textfromai) indextype is ctxsys.context; / CREATE OR REPLACE FUNCTION VISIONAI_RESULTS_TEXT_SEARCH(p_sql IN VARCHAR2) RETURN SYS_REFCURSOR AS refcursor SYS_REFCURSOR; BEGIN OPEN refcursor FOR select textfromai from AIVISION_RESULTS where contains ( textfromai, p_sql ) > 0; RETURN refcursor; END VISIONAI_RESULTS_TEXT_SEARCH; / BEGIN ORDS.ENABLE_OBJECT( P_ENABLED => TRUE, P_SCHEMA => 'AIUSER', P_OBJECT => 'VISIONAI_RESULTS_TEXT_SEARCH', P_OBJECT_TYPE => 'FUNCTION', P_OBJECT_ALIAS => 'VISIONAI_RESULTS_TEXT_SEARCH', P_AUTO_REST_AUTH => FALSE ); COMMIT; END; / In Conclusion This was a quick article showing an architectural pattern for developing data-driven AI apps by making calls to AI services directly from the database. Thanks so much for reading, and please let me know of any questions or feedback you may have.
The domain of Angular state management has received a huge boost with the introduction of Signal Store, a lightweight and versatile solution introduced in NgRx 17. Signal Store stands out for its simplicity, performance optimization, and extensibility, making it a compelling choice for modern Angular applications. In the next steps, we'll harness the power of Signal Store to build a sleek Task Manager app. Let's embark on this journey to elevate your Angular application development. Ready to start building? Let's go! A Glimpse Into Signal Store’s Core Structure Signal Store revolves around four fundamental components that form the backbone of its state management capabilities: 1. State At the heart of Signal Store lies the concept of signals, which represent the application's state in real-time. Signals are observable values that automatically update whenever the underlying state changes. 2. Methods Signal Store provides methods that act upon the state, enabling you to manipulate and update it directly. These methods offer a convenient way to interact with the state and perform actions without relying on observable streams or external state managers. 3. Selectors Selectors are functions that derive calculated values from the state. They provide a concise and maintainable approach to accessing specific parts of the state without directly exposing it to components. Selectors help encapsulate complex state logic and improve the maintainability of applications. 4. Hooks Hooks are functions that are triggered at critical lifecycle events, such as component initialization and destruction. They allow you to perform actions based on these events, enabling data loading, state updates, and other relevant tasks during component transitions. Creating a Signal Store and Defining Its State To embark on your Signal Store journey, you'll need to install the @ngrx/signals package using npm: But first, you have to install the Angular CLI and create an Angular base app with: JavaScript npm install -g @angular/cli@latest JavaScript ng new <name of your project> JavaScript npm install @ngrx/signals Creating a state (distinct from a store) is the subsequent step: TypeScript import { signalState } from '@ngrx/signals'; const state = signalState({ /* State goes here */ }); Manipulating the state becomes an elegant affair using the patchState method: TypeScript updateStateMethod() { patchState(this.state, (state) => ({ someProp: state.someProp + 1 })); } The patchState method is a fundamental tool for updating the state. It allows you to modify the state in a shallow manner, ensuring that only the specified properties are updated. This approach enhances performance by minimizing the number of state changes. First Steps for the Task Manager App First, create your interface for a Task and place it in a task.ts file: TypeScript export interface Task { id: string; value: string; completed: boolean; } The final structure of the app is: And our TaskService in taskService.ts looks like this: TypeScript @Injectable({ providedIn: 'root' }) export class TaskService { private taskList: Task[] = [ { id: '1', value: 'Complete task A', completed: false }, { id: '2', value: 'Read a book', completed: true }, { id: '3', value: 'Learn Angular', completed: false }, ]; constructor() { } getTasks() : Observable<Task[]> { return of(this.taskList); } getTasksAsPromise() { return lastValueFrom(this.getTasks()); } getTask(id: string): Observable<Task | undefined> { const task = this.taskList.find(t => t.id === id); return of(task); } addTask(value: string): Observable<Task> { const newTask: Task = { id: (this.taskList.length + 1).toString(), // Generating a simple incremental ID value, completed: false }; this.taskList = [...this.taskList, newTask]; return of(newTask); } updateTask(updatedTask: Task): Observable<Task> { const index = this.taskList.findIndex(task => task.id === updatedTask.id); if (index !== -1) { this.taskList[index] = updatedTask; } return of(updatedTask); } deleteTask(task: Task): Observable<Task> { this.taskList = this.taskList.filter(t => t.id !== task.id); return of(task); } } Crafting a Signal Store for the Task Manager App The creation of a store is a breeze with the signalStore method: Create the signalStore and place it in the taskstate.ts file: TypeScript import { signalStore, withHooks, withState } from '@ngrx/signals'; export const TaskStore = signalStore( { providedIn: 'root' }, withState({ /* state goes here */ }), ); Taking store extensibility to new heights, developers can add methods directly to the store. Methods act upon the state, enabling you to manipulate and update it directly. TypeScript export interface TaskState { tasks: Task[]; loading: boolean; } export const initialState: TaskState = { tasks: []; loading: false; } export const TaskStore = signalStore( { providedIn: 'root' }, withState(initialState), withMethods((store, taskService = inject(TaskService)) => ({ loadAllTasks() { // Use TaskService and then patchState(store, { tasks }); }, })) ); This method loadAllTasks is now available directly through the store itself. So in the component, we could do it in ngOnInit(): TypeScript @Component({ // ... providers: [TaskStore], }) export class AppComponent implements OnInit { readonly store = inject(TaskStore); ngOnInit() { this.store.loadAllTasks(); } } Harmony With Hooks The Signal Store introduces its own hooks, simplifying component code. By passing implemented methods into the hooks, developers can call them effortlessly: TypeScript export const TaskStore = signalStore( { providedIn: 'root' }, withState(initialState), withMethods(/* ... */), withHooks({ onInit({ loadAllTasks }) { loadAllTasks(); }, onDestroy() { console.log('on destroy'); }, }) ); This results in cleaner components, exemplified in the following snippet: TypeScript @Component({ providers: [TaskStore], }) export class AppComponent implements OnInit { readonly store = inject(TaskStore); // ngOnInit is NOT needed to load the Tasks !!!! } RxJS and Promises in Methods Flexibility takes center stage as @ngrx/signals seamlessly accommodates both RxJS and Promises: TypeScript import { rxMethod } from '@ngrx/signals/rxjs-interop'; export const TaskStore = signalStore( { providedIn: 'root' }, withState({ /* state goes here */ }), withMethods((store, taskService = inject(TaskService)) => ({ loadAllTasks: rxMethod<void>( pipe( switchMap(() => { patchState(store, { loading: true }); return taskService.getTasks().pipe( tapResponse({ next: (tasks) => patchState(store, { tasks }), error: console.error, finalize: () => patchState(store, { loading: false }), }) ); }) ) ), })) ); This snippet showcases the library's flexibility in handling asynchronous operations with RxJS. What I find incredibly flexible is that you can use RxJS or Promises to call your data. In the above example, you can see that we are using an RxJS in our methods. The tapResponse method helps us to use the response and manipulate the state with patchState again. But you can also use promises. The caller of the method (the hooks in this case) do not care. TypeScript async loadAllTasksByPromise() { patchState(store, { loading: true }); const tasks = await taskService.getTasksAsPromise(); patchState(store, { tasks, loading: false }); }, Reading the Data With Finesse Experience, the Signal Store introduces the withComputed() method. Similar to selectors, this method allows developers to compose and calculate values based on state properties: TypeScript export const TaskStore = signalStore( { providedIn: 'root' }, withState(initialState), withComputed(({ tasks }) => ({ completedCount: computed(() => tasks().filter((x) => x.completed).length), pendingCount: computed(() => tasks().filter((x) => !x.completed).length), percentageCompleted: computed(() => { const completed = tasks().filter((x) => x.completed).length; const total = tasks().length; if (total === 0) { return 0; } return (completed / total) * 100; }), })), withMethods(/* ... */), withHooks(/* ... */) ); In the component, these selectors can be effortlessly used: TypeScript @Component({ providers: [TaskStore], templates: ` <div> {{ store.completedCount() } / {{ store.pendingCount() } {{ store.percentageCompleted() } </div> ` }) export class AppComponent implements OnInit { readonly store = inject(TaskStore); } Modularizing for Elegance To elevate the elegance, selectors, and methods can be neatly tucked into separate files. We use in these files the signalStoreFeature method. With this, we can extract the methods and selectors to make the store even more beautiful. This method again has withComputed, withHooks, and withMethods for itself, so you can build your own features and hang them into the store. // task.selectors.ts: TypeScript export function withTasksSelectors() { return signalStoreFeature( {state: type<TaskState>()}, withComputed(({tasks}) => ({ completedCount: computed(() => tasks().filter((x) => x.completed).length), pendingCount: computed(() => tasks().filter((x) => !x.completed).length), percentageCompleted: computed(() => { const completed = tasks().filter((x) => x.completed).length; const total = tasks().length; if (total === 0) { return 0; } return (completed / total) * 100; }), })), ); } // task.methods.ts: TypeScript export function withTasksMethods() { return signalStoreFeature( { state: type<TaskState>() }, withMethods((store, taskService = inject(TaskService)) => ({ loadAllTasks: rxMethod<void>( pipe( switchMap(() => { patchState(store, { loading: true }); return taskService.getTasks().pipe( tapResponse({ next: (tasks) => patchState(store, { tasks }), error: console.error, finalize: () => patchState(store, { loading: false }), }) ); }) ) ), async loadAllTasksByPromise() { patchState(store, { loading: true }); const tasks = await taskService.getTasksAsPromise(); patchState(store, { tasks, loading: false }); }, addTask: rxMethod<string>( pipe( switchMap((value) => { patchState(store, { loading: true }); return taskService.addTask(value).pipe( tapResponse({ next: (task) => patchState(store, { tasks: [...store.tasks(), task] }), error: console.error, finalize: () => patchState(store, { loading: false }), }) ); }) ) ), moveToCompleted: rxMethod<Task>( pipe( switchMap((task) => { patchState(store, { loading: true }); const toSend = { ...task, completed: !task.completed }; return taskService.updateTask(toSend).pipe( tapResponse({ next: (updatedTask) => { const allTasks = [...store.tasks()]; const index = allTasks.findIndex((x) => x.id === task.id); allTasks[index] = updatedTask; patchState(store, { tasks: allTasks, }); }, error: console.error, finalize: () => patchState(store, { loading: false }), }) ); }) ) ), deleteTask: rxMethod<Task>( pipe( switchMap((task) => { patchState(store, { loading: true }); return taskService.deleteTask(task).pipe( tapResponse({ next: () => { patchState(store, { tasks: [...store.tasks().filter((x) => x.id !== task.id)], }); }, error: console.error, finalize: () => patchState(store, { loading: false }), }) ); }) ) ), })) ); } This modular organization allows for a clean separation of concerns, making the store definition concise and easy to maintain. Streamlining the Store Definition With selectors and methods elegantly tucked away in their dedicated files, the store definition now takes on a streamlined form: // task.store.ts: TypeScript export const TaskStore = signalStore( { providedIn: 'root' }, withState(initialState), withTasksSelectors(), withTasksMethods(), withHooks({ onInit({ loadAllTasksByPromise: loadAllTasksByPromise }) { console.log('on init'); loadAllTasksByPromise(); }, onDestroy() { console.log('on destroy'); }, }) ); This modular approach not only enhances the readability of the store definition but also facilitates easy maintenance and future extensions. Our AppComponent then can get the Store injected and use the methods from the store, the selectors, and using the hooks indirectly. TypeScript @Component({ selector: 'app-root', standalone: true, imports: [CommonModule, RouterOutlet, ReactiveFormsModule], templateUrl: './app.component.html', styleUrl: './app.component.css', providers: [TaskStore], changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppComponent { readonly store = inject(TaskStore); private readonly formbuilder = inject(FormBuilder); form = this.formbuilder.group({ taskValue: ['', Validators.required], completed: [false], }); addTask() { this.store.addTask(this.form.value.taskValue); this.form.reset(); } } The final app: In Closing In this deep dive into the @ngrx/signals library, we've unveiled a powerful tool for Angular state management. From its lightweight architecture to its seamless integration of RxJS and Promises, the library offers a delightful development experience. As you embark on your Angular projects, consider the elegance and simplicity that @ngrx/signals brings to the table. Whether you're starting a new endeavor or contemplating an upgrade, this library promises to be a valuable companion, offering a blend of simplicity, flexibility, and power in the dynamic world of Angular development. You can find the final code here. Happy coding!
Artificial Intelligence (AI) and Machine Learning (ML) have revolutionized the way we approach problem-solving and data analysis. These technologies are powering a wide range of applications, from recommendation systems and autonomous vehicles to healthcare diagnostics and fraud detection. However, deploying and managing ML models in production environments can be a daunting task. This is where containerization comes into play, offering an efficient solution for packaging and deploying ML models. In this article, we'll explore the challenges of deploying ML models, the fundamentals of containerization, and the benefits of using containers for AI and ML applications. The Challenges of Deploying ML Models Deploying ML models in real-world scenarios presents several challenges. Traditionally, this process has been cumbersome and error-prone due to various factors: Dependency hell: ML models often rely on specific libraries, frameworks, and software versions. Managing these dependencies across different environments can lead to compatibility issues and version conflicts. Scalability: As the demand for AI/ML services grows, scalability becomes a concern. Ensuring that models can handle increased workloads and auto-scaling as needed can be complex. Version control: Tracking and managing different versions of ML models is crucial for reproducibility and debugging. Without proper version control, it's challenging to roll back to a previous version or track the performance of different model iterations. Portability: ML models developed on one developer's machine may not run seamlessly on another's. Ensuring that models can be easily moved between development, testing, and production environments is essential. Containerization Fundamentals Containerization addresses these challenges by encapsulating an application and its dependencies into a single package, known as a container. Containers are lightweight and isolated, making them an ideal solution for deploying AI and ML models consistently across different environments. Key containerization concepts include: Docker: Docker is one of the most popular containerization platforms. It allows you to create, package, and distribute applications as containers. Docker containers can run on any system that supports Docker, ensuring consistency across development, testing, and production. Kubernetes: Kubernetes is an open-source container orchestration platform that simplifies the management and scaling of containers. It automates tasks like load balancing, rolling updates, and self-healing, making it an excellent choice for deploying containerized AI/ML workloads. Benefits of Containerizing ML Models Containerizing ML models offer several benefits: Isolation: Containers isolate applications and their dependencies from the underlying infrastructure. This isolation ensures that ML models run consistently, regardless of the host system. Consistency: Containers package everything needed to run an application, including libraries, dependencies, and configurations. This eliminates the "it works on my machine" problem, making deployments more reliable. Portability: Containers can be easily moved between different environments, such as development, testing, and production. This portability streamlines the deployment process and reduces deployment-related issues. Scalability: Container orchestration tools like Kubernetes enable auto-scaling of ML model deployments, ensuring that applications can handle increased workloads without manual intervention. Best Practices for Containerizing AI/ML Models To make the most of containerization for AI and ML, consider these best practices: Version control: Use version control systems like Git to track changes to your ML model code. Include version information in your container images for easy reference. Dependency management: Clearly define and manage dependencies in your ML model's container image. Utilize virtual environments or container images with pre-installed libraries to ensure reproducibility. Monitoring and logging: Implement robust monitoring and logging solutions to gain insights into your containerized AI/ML applications' performance and behavior. Security: Follow security best practices when building and deploying containers. Keep container images up to date with security patches and restrict access to sensitive data and APIs. Case Studies Several organizations have successfully adopted containerization for AI/ML deployment. One notable example is Intuitive, which leverages containers and Kubernetes to manage its machine-learning infrastructure efficiently. By containerizing ML models, Intuitive can seamlessly scale its Annotations engine to millions of users while maintaining high availability. Another example is Netflix, which reported a significant reduction in deployment times and resource overheads after adopting containers for their recommendation engines. Conclusion While containerization offers numerous advantages, challenges such as optimizing resource utilization and minimizing container sprawl persist. Additionally, the integration of AI/ML with serverless computing and edge computing is an emerging trend worth exploring. In conclusion, containerization is a powerful tool for efficiently packaging and deploying ML models. It addresses the challenges associated with dependency management, scalability, version control, and portability. As AI and ML continue to shape the future of technology, containerization will play a pivotal role in ensuring reliable and consistent deployments of AI-powered applications. By embracing containerization, organizations can streamline their AI/ML workflows, reduce deployment complexities, and unlock the full potential of these transformative technologies in today's rapidly evolving digital landscape.
Making Better Decisions in a Busy World
January 9, 2024
by
CORE
C4 Model Perspective: The Different Types of Software Architects
January 8, 2024 by
Postgres Full-Text Search With Hibernate 6
January 9, 2024
by
CORE
Explainable AI: Making the Black Box Transparent
May 16, 2023 by
Demystifying Cloud Trends: Statistics and Strategies for Robust Security
January 9, 2024 by
Low Code vs. Traditional Development: A Comprehensive Comparison
May 16, 2023 by
Postgres Full-Text Search With Hibernate 6
January 9, 2024
by
CORE
Demystifying Cloud Trends: Statistics and Strategies for Robust Security
January 9, 2024 by
How To Differentiate Cloud Types: Public vs. Private vs. Hybrid in 2024
January 9, 2024 by
Low Code vs. Traditional Development: A Comprehensive Comparison
May 16, 2023 by
Postgres Full-Text Search With Hibernate 6
January 9, 2024
by
CORE
The Technology Powering Trading Signals in Binary Options: A Deep Dive
January 9, 2024 by
Five IntelliJ Idea Plugins That Will Change the Way You Code
May 15, 2023 by