
I am a Senior Consultant, Application Developer at Thoughtworks with more than 7 years of experience in building highly efficient and scalable applications using agile methodologies, clean coding and best practices for large enterprises. I develop applications using micro-services and event driven architecture, consult companies on OO Design, patterns, testing techniques and development methodologies. Passionate about XP and agile
AI Software Engineer (Contract)
MercorApplied AI Learning & Projects
IndependentTechnical Lead
Avalara TechnologiesSenior Research Engineer
Hyundai MobisSenior Software Engineer
Thoughtworks
PostgreSQL
AWS (Amazon Web Services)

Java

Apache Kafka

Azure Cosmos DB
.png)
Docker

Java 11

Java 8

Spring Cloud

Spring Boot

Azure Pipelines

GitHub

IntelliJ IDEA

Azure Active Directory

New Relic
.jpg)
Grafana

Splunk
Cucumber
.png)
Jenkins
REST API
.jpg)
Web API

AWS
Azure

MySQL

Restful API

Kafka

Kubernetes

Gradle

Maven

Postgres

MongoDB

CosmosDB

Nomad

Rio

Amazon EKS

PCF

Maven

Azure DevOps

Azure Cloud

AWS Cloud

Consul
.png)
Datadog

Github
Hi, all. I'm Akash. I have nine plus years of professional experience. I'm currently working as a senior software engineer at Avaya Technologies, and previously I worked with Okta as a senior consultant. I'm primarily a back-end engineer who likes to work on Java-related technologies. And, I've been working on domains like e-commerce, banking, healthcare, etcetera.
The so the difference between primary key and unique key is that primary keys are unique keys, but all unique keys are not primary keys. The purpose of primary key is to uniquely identify a record whenever new records are inserted in a table. A unique key, on the other hand, is a key which can be used, which is unique across all the records. So, basically, all primary keys are unique keys, but all unique keys are not primary keys. Primary keys can be auto-generated or manually generated during insertion, whereas unique keys have to be always generated from the user side.
Both arrays and stacks are data structures, basically, which store the data in a linear fashion. But the difference is arrays are something which you can access the elements via indexes. They are stored contiguously. I mean, they're stored in a contiguous memory, and they have one access time. Once you create an array, you initialize an array, you cannot change its size. Whereas the stack is an abstract data structure, implemented over other primitive data structures. Their size can grow after initialization. And they store data linearly, but the access way of accessing is different. So, basically, in arrays, you can access any of the elements. But whereas in a stack, you cannot access any random element. You can only access the top element. And they follow LIFO structure, basically. So the last inserted element can only be extracted out. It's basically last in, first out. And they serve from different purposes, basically. In what scenario? Basically, whenever you want to store the state of any events, you push in the stack. For example, if you visit a website and you have a track of web pages, then stacks are used to store when you go back and forward from the web pages, their order is stored in a stack. So the last page which you have visited, if you press the back button, you go to the previous page from where you came. So you cannot access the random pages when you use forward and back buttons. But whereas some arrays are something where you just store, let's say, a list of records or a list of objects, and you just want to access it randomly in one time. So they both serve different purposes, and stacks, as an abstract data structure, can be implemented over arrays, lists, etc.
So the difference between Java seven and twenty one and twenty two are a lot. Basically, Java seven is the oldest stable Java version. And, after that, there are a lot of many versions, LTE versions introduced. In older Java seven versions, there was no functional style of programming. They were plain, the objects and the collections were used as simple collections. There were no lambdas and streams. So a lot of memory improvement happened after Java seven. In latest versions, like twenty two, they also use heavily functional style of programming. They have, like, variables, vars, where you don't need to declare the type of the variable. Also, from a memory point of view, there are a lot of JVM improvements, AOT and JIT compiler improvements, memory model improvements. Earlier in Java seven, you did not have virtual threads. Now in Java twenty one twenty two, you have virtual threads which help you efficiently manage memory and resources in multi programming or multithreading. Also, in latest Java, like twenty one and twenty two, you have sealed classes where you can prevent inheritance of those classes. You can control the inheritance. You can create specific records and tightly control them in a tightly controlled manner. Also, you have flexible switches where you can call different functions of different types. Switch cases are now more flexible. They are not tied to a particular data type. And a lot of coding style and syntax improvements are there. Now the code focus in Java twenty one twenty two is more on the declarative style of programming rather than imperative. So you spend less time on writing syntax and telling how to do it rather than the focus is on what to do.
So the problems in this particular code I see is, there is no error handling, in the current design. And, if messages processor throws an exception, Kafka may repeatedly deliver the messages in an infinity loop or commit offsets prematurely. Also, there is no idempotency. In the first message, it is reprocessed due to retries and consumer restarts. Side effects like DB writes or API calls may execute multiple times. Also, there is no acknowledgment of control. So by default, Spring Kafka commits offsets automatically, like in batch or record mode, depending on the config. So if the processing fails, basically, after committing the offset, the messages are lost. And, also, there is no dead letter queue, so failing messages stay in the topic indefinitely and block the partition preventing other messages from being processed. There is no logging present, like silent failures. You cannot know what has gone wrong. There is no back pressure handling control. So, if messages read a spike, the consumer may overload or crash down. So, I think these are some of the issues in this current code of implementation.
So the potential issues with the very large input strings for this particular type of program is that there is high memory usage when on the spin space time complexity, and each opening bracket pushes into a new stack for extremely large inputs, like millions of characters. This consumes a lot of heap memory. Possible consequences could be out of memory errors, or heavy garbage collection pressure because in the JVM, they constantly need to monitor and then remove those data. There also will be performance overhead of the stack class, because Java's little dot stack is basically synchronized, which adds unnecessary locking overhead and in single-threaded methods. So for large inputs, this slows down processing. There is boxing overhead also because stack is using character. If the stack uses the wrapper character class, then converting from primitive to non-primitive and non-primitive to primitive could be an extra overhead and too much load on the GC. So to optimize this, basically, we can use array dequeues, which are non-synchronized and memory efficient, to avoid synchronization overhead. And further optimizations can be the data can be streamed, processed the stream chunks by chunks, and from the reader, or a stream, instead of loading the entire stream. Early exit optimizations, like if you know how the input is open brackets with no chance of matching, then imbalance already visible, we can abort the program early. You can have custom primitive stacks like a primitive char array stack to completely avoid object boxing or an unboxing.
So the important design considerations, what in this we can make is, can the caller realistically recover from the error? Yes. So if you use a checked exception like IO exceptions, for example, file not found, a missing config file, or temporary network issue, then the caller might retry showing that error message or prompt via a valid path. If there are no such opportunities, that means the program cannot or should not recover, so use an unchecked exception. For example, misconfiguration, developer bugs, or logical error. Sorry. So the second point is, near responsibility and abstraction boundary. So, lower level libraries, infra or I/O DB networking should throw checked exceptions. They expose the real world expected failures. The caller decides what to do. Then higher level business logic or service can wrap checked exceptions into domain specific unchecked exceptions. This hides low level details and simplifies upper layer handling. Then, the third point is, API usability versus robustness trade off. Checked exceptions encourage robust error handling, but can make APIs verbose and hard to use. Unchecked exceptions make code cleaner, but risk runtime crashes. So, basically, the rule of thumb is library or infrastructure code, we should prefer checked exceptions. We should inform the caller early. And application or domain layer, we should prefer unchecked exceptions. For the fourth point, we should have consistency, whatever we choose, be consistent across the code base or library. If some methods are thrown checked exceptions and another wrap them, it leads to confusion and improper handling. And so, basically, in summary, the key design decisions depend on who can handle the error and how recoverable it is. If the caller has recovered, like, retrying or provide a new file path through a checked exception, such as IO exception. If the error is unrecoverable or you want to abstract away from low level details, wrap it in an unchecked exception, ideally domain specific ones, not something raw like runtime exception.
In one of my back-end projects, I applied the factory pattern in combination with the singleton pattern for a notification service that supported multiple channels, including emails, WhatsApp, and SMS. So each channel had a different integration provider like SMTP, Twilio, Meta APIs, etc. And I needed a clean way to instantiate the correct sender without cluttering the business logic with if-else or switch statements. So I created a notification factory class responsible for returning the right implementation of the notification center and interface based on the channel type. The factory pattern helps centralize object creation logic. So adding new channels like Slack or push notification required no change in existing service logic, just a new class implementing the interface. A single method ensured that only one factory instance existed across the application, reducing unnecessary object creation and improving performance. Overall, these patterns made the design more maintainable and scalable as a system design.