Over the course of the past 18 months, we have been involved in no fewer than a dozen engagements where large, mostly monolithic applications needed to be re-architected, moved to the cloud, and integrated with newly acquired applications. Most of these engagements involved companies with over 100 people in R&D, over $100M in revenue, over 3-4 applications developed independently (usually through acquisition), and over 5 million lines of code.
Point-to-point web services APIs over HTTP, proprietary RPC APIs, direct linking (old and bad), and database access APIs (ODBC, JDBC) have become yesterday’s methodologies for integrating applications. Most of these approaches yield tightly coupled systems replete with mutual dependencies. A client component often cannot survive an upgrade of the target application without breaking. This situation is true whether we are talking about two components within a singular vendor or two applications built by two different vendors attempting to integrate. Even fairly modern REST or SOAP web services can suffer from these problems.
A New Era
There were several reasons for staying away from ESB architectures, particularly for ISVs. First, ESBs were expensive. Software provided by ESB vendors like Tibco and Progress/Sonic is not cheap. Furthermore, standing up and configuring an ESB was not and is not trivial. When ISVs would try to OEM an ESB, they would often run into support nightmares – having to hand-hold customers through ESB configuration, debugging, and maintenance.
In today’s world, both problems have been significantly mitigated. Firstly, there are plenty of low-cost, free, and open source ESBs. Secondly, as most enterprise application ISVs are primarily SaaS, their customers don’t have to worry about standing up and configuring an ESB – that responsibility falls to the ISV’s cloud ops team.
Finally, ESBs have become somewhat commoditized, and are now offered in PaaS (platform-as-a-service) configurations by both Amazon AWS and Microsoft Azure, making it incredibly easy to stand up, configure, provision, and run an enterprise-grade ESB. A quick browse of the Azure and AWS catalogues will result in finding Azure Message Bus, ActiveMQ, RabbitMQ, Kafka, AWS SQS, and several other enterprise grade products.
What is Loose Coupling?
In the domain of software architecture, coupling is a characteristic that defines the degree to which components of a system depend on one another. Tightly coupled architectures are composed of components that require detailed knowledge of other collaborating components, either within the same application or with another application via programmatic integration, to perform their purpose.
In a tightly coupled system, cross-dependencies are codified into the components themselves, and therefore any changes to the behavior of any one component often requires changes to components across the system. Additionally, components in a tightly coupled architecture often require a real-time synchronous approach to communications to ensure control flow occurs as expected by the programmers who wrote the code.
By comparison, a loosely coupled architecture is composed of elements that can stand independently and are resilient to changes in the behavior of components with which they collaborate. Communications between components are most often conducted using an asynchronous channel. This allows components to process events and messages on their own terms without impacting the operation of the component that sent the event or message.
Why Does Loose Coupling Matter?
Loose coupling can greatly improve application scalability, resilience, maintainability, and extensibility. Scalability improves in two dimensions – firstly, loosely coupled components are cloned as needed to handle additional demand thus scaling “out” capacity, and secondly one can further decompose components into smaller functional units to provide additional leverage for scaling up to higher levels of load.
Due to the event-driven nature of the architecture, a system utilizing loosely coupled components that communicate asynchronously is much more resilient when compared to a monolithic system with synchronous communications. Asynchronous communication avoids the waiting that one component does after it communicates with another component. When layers of software are tied together synchronously, the “waiting” for responses of this integration adds up and often results in unintended bottlenecks that any one designer of any one component could not predict and thus factor into their design.
Tightly bound application architectures tend to accrete complexity as they move through their lifecycle. This tendency is so prevalent that developers have coined a term for the resulting system – a Big Ball of Mud (BBoM). When a development team is asked to fix a bug or extend a feature in a BBoM, the resulting effort can span across the entire system and prove to be very costly to accomplish – an effort known amongst developers as shotgun surgery. By comparison, a well-designed loosely coupled architecture will typically only require changes to discrete components, and those changes will not have cascading effects across the system. This more focused effort is both less costly to develop and less risky to implement into production.
How Hard Is It to Implement?
Implementing a loosely coupled architecture is achievable with any application. The complexity of doing so is based off two main factors: size of existing codebase and mindset of the developers. The developer mindset when working toward a loosely coupled design must be focused on data flow, and not as focused on control flow. Control flow is a critical detail within each component of a system, but data flow should be a programmer’s / architect’s focus when designing at the system level.
An architect experienced with building loosely coupled systems is needed to lead the effort if one wants to move quickly and avoid pitfalls. If one is working with a large or complex monolithic application, a good approach to application transformation is first to identify business capabilities present in the application and locate “seams” where those capabilities can be isolated and componentized.
Further steps would involve:
- Writing tests to baseline the behavior required for fulfilling the selected business capability (this type of test is often called a pinning test as it “pins”, or isolates, the desired behavior)
- Exposing the behavior of the component via an application programming interface, which may be available via a message bus topic, a web services interface, or another communication channel
- Ensuring a new component can communicate with legacy applications and that it behaves in a manner consistent with the legacy code being replaced (these are sometimes called canary tests)
- Routing production traffic to the new component and deprecating the legacy code
- Selecting the next business capability in the legacy application to be refactored out into an isolated and loosely coupled component – this brings you back to the first step, and this cycle will repeat until the desired level of transformation has been completed
Envision the integration of an ERP system with a CRM system, two components of a large software application many of which might have been acquired over time. A common scenario is to push a record of a Purchase Order to the CRM system so that any salesperson viewing an account in the CRM system can see all of the orders placed by such a customer.
In a tightly coupled world, the ERP system, upon creating and accepting a Purchase Order, would make an API call through direct linking, ODBC/JDBC, or even a point-to-point REST or SOAP web services call to the CRM system. The call would most likely be synchronous, meaning that the ERP system (and possibly the end-user who submitted the activity through a user interface) would be waiting while the CRM system processed the request and a success or error code is returned.
In order for this to work, the ERP system would have to know where the CRM system is (hostname or IP address) and the CRM system would have to be up and running. The network between the CRM and ERP system would have to be intact. If the CRM system was particularly busy (maybe because it was processing many requests at that time), the ERP system would have to wait and wouldn’t be able to complete its action until the CRM system was able to successfully handle the request. Finally, if the CRM system is updated with a new version, there is a chance the ERP system would break if the integration wasn’t extremely well designed. The new version of the CRM system couldn’t ship or be deployed until a new version of the ERP system was shipped and deployed.
In a loosely coupled world, an ESB with a pub/sub pattern could be used. The ERP system would publish an event such as “Purchase Order Accepted”. Any system interested in that event would subscribe to it. More than one system could subscribe – maybe both a CRM system and a Data Warehouse would subscribe.
The CRM system would listen for the event, and when it received the event would store an instance of the purchase order (maybe only a subset of the information that it needed for CRM purposes). The ERP system wouldn’t wait for the CRM system to acknowledge success. Instead, the ESB would take care of guaranteeing that the message would be delivered. In the case where the CRM system is busy or offline, the ESB would retry, and eventually, the CRM system would get the message. The initiating system, in this case, wouldn’t be responsible for handling the complexity of error conditions, and thus, its code would be faster and simpler. Instead, the error handling falls to a combination of the ESB and the consumer – the CRM system. To handle the numerous nuances in use cases, a typical ESB has a variety of patterns that determine whether messages are synchronous or asynchronous, how many consumers can consume a message, and how to handle versioning of message types or “topics”.
Next generation applications built with a loosely coupled architecture can leverage third-party services, scale in a cost-efficient manner, and provide their development teams with the agility needed to compete in the modern software economy. The approach embodied in a software architecture is becoming a competitive differentiator that should not be ignored. It provides investors and executives with new alternatives for modernizing and integrating existing and legacy components. Loosely coupled applications are here now and will continue to flourish in the future and will be an important part of post-merger technology integration as well as large-scale application development.