The unseen Connascence in modern web applications
Ideal web applications should render the latest state of visible information. This constraint demands client applications to know what changes after a user’s operation; thus, any possible out-dated data can be refreshed. The fact that the client has to know what the server updates on each business operation introduce a form of Connascence between them.
If you have worked building web applications for a while you probably know what I’m talking about after you read the above resume. If not, it’s also fine. This problem has lived unseen for a while, making us get used to it and thinking it’s something we have to live with. In order to avoid any confusion on the understanding of the problem, that could lead us to inaccurate solutions, I will explain in deep detail the problem to make sure you, the reader, know what truly is and what is not.
The keeping-in-sync problem
Figure above describes a workflow of the way of work of Single Page Applications. Regardless of the library or framework you use, the workflow will be the same:
- A user requests to load a page or UI section.
- Multiple endpoints are queried in the web service to get the information the user needs to see.
- The responses from the web service are store locally in the SPA. This local store can live outside the UI components, like in Figure 1, or they can reside inside. Regardless of the approach, you will always have a local state in the web app.
- UI components read the local state and based on it composes different UI sections with the information.
- A consolidated page is generated and shown to the user.
The takeaway here is that SPA contains a local state that works as a source of truth of the content they show. At the same time, an external service contains the real state of the system, and the SPA constantly queries the service’s state in order to show up-to-date information.
Besides consuming information. users can also change the information. Web applications provide ways for users to alter the information based on pre-defined business processes. This can be trivial as adding an item to the shopping car on an e-commerce website, or complex as canceling a flight in an internal booking system of an airline. Depending on the complexity of the business process, the operation might update different sources of information and it’s the UI app in charge of knowing what resources such operation changes in order to refresh its local state and keep the visible information in sync with the state of the system.
As mentioned before, the business process related to the operation might be complex resulting in the change of not one but multiple different sources of information. Therefore the UI app needs to refresh any visible information that comes from those updated sources.
This does not see a real problem in not-complex operations, such as adding a new item to a shopping car since probably the only data we need to update is the list in the shopping car. However in the previous example of the internal booking system of an airline, besides updating the flight status there might be multiple sections that need to be updated as well. This of course depends on the complexity of the interface and internal systems tend to requires such complexity.
async cancelFlight(flightNumber) {
await externalService.cancelFight(flightNumber)
refreshFlightStatus()
refreshStatusHistory()
refreshOperationCosts()
refreshAnyOtherInfoAffected()
}
So in general exists a many-to-many relation between operations and resources that come from the service. This relation borns from the definition of the business process the operation fulfills.
The big deal with this is that if the UI apps want to properly show always the up-to-date state of its visible information, they have to encode this relation in its source code in order to know what has to be updated afterward. if there is a Business process that is triggered with the operation O1 and such operation updates the resources {R1, R2, R3}, and its UI shows information regarding those three resources, they will have to be refreshed after operation O1.
So the problem comes from the fact the UI app needs to be aware of this mapping between operations and resources that are affected by such operations. As stated before this relation borns from the implementation of a business process and since business processes are implemented in the service layer, it’s logical to define this relation in the service where all the business logic lives. However, now UI apps require to know this business mapping in order to keep their interfaces up-to-date after an operation. The result of this is a coupling or a form of Connascence between two far apart codebases: the service and the UI app.
As with any form of coupling, tit affects the flexibility of a software system to support new changes. If originally the system contains an operation O1 that updates resources {R1, R2, R3}, but later it requires the system to be updated in a way O1 updates also R4; it’s not enough to update the business layer in the service to support this change. It’s also needed to update the UI app to refresh R4 after operation O1. Omitting this would result in the typical bug: “After operation O1, the section of R4 is not updated but only after the user manually refreshes the page”.
Even worse there might happen that the operation does not longer change a resource that was previously updated. For instance, following our latest example, suppose the system is required to be updated such as O1 now updates resources {R1, R3}. Notice there is not new resource added on the list of updates of O1 therefore there is not additional refresh to implement in the UI app. However, R2 is no longer updated by operation O1, so it would be easy to forget this unnecessary update of R2 in the code, in the end, it will not produce a bug. Image this kind of changes in current start-up companies that need to reinvent themself on a daily basis and thus update their business processes constantly. After a long-running of the UI app, it would not be unusual to have a codebase with unnecessary refresh code scattered all over the files to which developers usually react with “Why this exists if it’s doing nothing?”.
With these two latest examples, I have intended to describe a frequent case of coupling between UI apps and their services. This coupling borns from the need for UI apps to always show up-to-date information to the users throughout the heuristic approach of coding in their source code, which information changes after the operation is executed in the service (operation => resource mapping described in latest figure). As with any coupling, it increases the maintainability costs of a system and the proportion of increase will depend on the strength, degree, and locality of the coupling, something that I will discuss in the next section.
For the sake of facilitating the readability of the subsequent information, I will use from this point now forward the term “In-sync coupling” to refer to the type of coupling described before.
Analyzing coupling throughout the eyes of Connascence
From the Internet you can find the following definition of Connascence:
- “Connascence is a software quality metric & a taxonomy for different types of coupling.”
- “In software engineering, two components are connascent if a change in one would require the other to be modified in order to maintain the overall correctness of the system”
Probably those definitions might not make sense for a person who hears the concept for the first time. If that’s the case I suggest reading more about it on the Internet before continuing, I assure you would not be disappointed. The takeaway here is that Connascence is a metric for coupling and gives us a framework to categorize different kinds of coupling based on how badly they affect the flexibility/ability-to-change of a system.
When analyzing a Connascence, to determine how affects flexibility, it’s useful to base the analysis on its three properties:
- Strength. It’s determined by the ease with which that type of coupling can be refactored.
- Degree. An entity that is connascent with thousands of other entities is likely to be a larger issue than one that is connascent with only a few.
- Locality. Connascent elements that are close together in a codebase are better than ones that are far apart.
These three properties altogether contribute to the weight of the Connascence in the same way a vector gets its magnitudes in a three-dimension space.
Knowing the properties of Connascences is important since reducing any of its properties would mean reducing the impact of such Connascence. That means we could translate the problem of reducing the Connascence to reducing one or more of its properties in order to decrease the coupling produced in the system. With this in mind, let’s analyze the In-sync coupling using these three concepts.
Strength on “In-sync” coupling
Connascense provides a set of categories for the strength of coupling. Not all forms of coupling produce the same negative effects in a system. One forms of Connasence are worse than others therefore reducing the Strength of the Connascense would mean transforming strong forms into weak forms.
If you followed my previous advice and read about Connascense then you will know the description of these types of Connascense. Based on what I have described in the previous section I categorize the “In-sync” coupling as Connascense of Algorithm. Implementing an operation in the service means implementing an algorithm that fulfills the needs from a product perspective. Changing this algorithm would mean potentially changing the information the user perceives thus, the UI app would need to be aware of the change.
as you can see in the previous diagram, Connascense of Algorithm is not that bad. Improving it would mean transform it into Connascense of Convention (you can also find it under the name Connascense of Meaning), Type, or Name.
Degree on “In-sync” coupling
it relates to the degree of its occurrence. An element that is connascent with thousands of other elements is likely to be a larger issue than one that is connascent with only a few. We can use the following example from Wikipedia to describe the degree of Connascense of Position that is observed in the parameters of a procedure: “a function or method that takes two arguments is generally considered acceptable. However, it is usually unacceptable for functions or methods to take ten arguments.”
The “in-sync” coupling can be found at most of the operations exposed in systems. Unless the operation does not involve changing the state of the system but emitting events outside its boundary, such as sending an email or an event to Slack, it will imply updating the state of any information the final user sees. Therefore the degree of this coupling will be proportional to the number of operations the UI app exposes to the user.
Locality on “In-sync” coupling
The locality of an instance of connascence is how close the two coupled entities are to each other. Code that changes together should live together. Code that is close together (in the same module, class, or function) should typically have more, and higher forms of connascence than code that is far apart (in separate modules, or even codebases). This means we should move code with strong connascence closer together whenever possible.
The “in-sync” coupling has a high locality. The coupling is between two separated applications, the UI app, and its service. As mentioned before, having a connascence with a high locality is not a big problem when its strength is weak. Although the “in-sync” coupling does not have a strong form of connascence, it hasn’t a weak form neither; remember we define it as a Connascense of Algorithm. Therefore there is room for improvement by reducing the strength of this high locality connascence to make it “acceptable” and thus reducing its complexity.
Reducing “in-sync” coupling’s locality
What if the UI app didn’t have to know what to update after an operation? what if instead, the service is in charge of telling the UI what has been changed after an operation? Indeed this is the approach I would like to take. By moving the knowledge of “what to refresh after an operation” to the service, we are reducing the locality of the coupling under discussion and thus reducing its complexity. To achieve this the service will have to change the way it responses to operation requests. Reusing our previous example of an operation O1 that updates resources {R1, R2, R3}, the following would be the potential response:
REQUEST POSTURL: https://my-service.com/api/any/path/operation-O1body: { ...payload }RESPONSEbody: {
"result": { ...operation-related-info },
"side-effects": [
https://my-service.com/api/any/path/R1-uuid,
https://my-service.com/api/any/path/R2-uuid,
https://my-service.com/api/any/path/R3-uuid,
.
.
.
]
}
in the previous fragment, we can see the prospective way of communicating to the UI app what has to be updated after the execution of an operation. The key part is the “side-effects” list we are getting. This is the information that tells the UI app what has been changed so it can be refreshed. It’s worth noting that this approach assumes each resource has a Uniform Resource Identifier (URI) in the API. Throughout this approach, the UI app can be completely unaware of what O1 is doing in the service. It doesn’t longer need to know what side effects O1 produces in the system since the service will be in charge of communicating them. In fact, this new approach allows us to create more declarative UI apps which I believe is the ultimate goal web development wants to reach.
Having a dynamic way of knowing what to refresh enables us to implement a general framework to refresh information after operations. This framework will process the “side-effects” entries and refresh only the visible resources in the UI app without any implementation from the developer. This is an overview of the new workflow when operations are trigger in the UI:
In the previous diagram, we introduced a service call interceptor that it’s needed for intersecting the responses from each executed operation. Then it will read the side effects received and get the new state of the updated resources. and finally will update the data in the local states which will make the UI components show the new information.
There are some additional considerations to keep in mind. The interceptor should refresh a resource from a side-effect list only if there is currently at least one component rendered that shows information about such resource. We want to avoid doing unnecessary query operations to the service so we should only refresh what it’s visible to the user. This means the interceptor must know if a specific resource is currently rendered in the application. We could achieve this by implementing subscriptions in the local states. Whenever a component needs to read information from a local state, it first subscribes to it, thus local states can track who is reading its data; Then the interceptor can ask the local state if there is anyone reading information about that resource using its type and ID.
Considerations in the Service
The new information that should be returned in the response from the Service it’s only important for UI applications. As I explained before, this is a way of simulating reactivity when an action that mutates state is triggered in the service. This means the implementation and its incurred overhead should only be for APIs used in UI apps. Don’t implement this in the core business layer of your application, otherwise, any other client who does not need it will be affected by its overhead. Instead, the implementation should live in a part of the API layer of your service. The implementation can happen as an aggregated service that sits between the clients and the server, as GraphQl operates.
Can event-based communication solve the problem?
Asynchronous event communication is useful in Real-Time Applications. This approach does not refer to such applications. If another user does updates on the current information I’m seeing I might not want to see those updates to appear seamless. The type of problem we have described is about showing the actual state of the system after an operation of the user. Showing the actual state of a system at any time is a completely different problem. Additionally, event-driven communication increases the complexity of a system in comparison with a regular request/response schema. Not all systems demand such complexity.
Summary
First, we started by describing a common form of coupling between UI client applications and their services, which I called the “in-sync” coupling. This coupling is born from the need for UI apps to show the system’s up-to-date state throughout the empirical approach of hardcoding in its source code what changes after an operation. And like any form of coupling, it increases the maintainability cost of the software.
Later, we use Connascence to analyze the coupling and find a way to reduce its damage to a system’s flexibility. Our analysis demonstrated we could reduce the complexity of the “in-sync” coupling by reducing its locality.
Finally, we described reducing the coupling’s locality by letting the service communicate to the UI clients what has changed after an operation using the response’s payload. This approach does not only reduce the dame of the “in-sync” coupling but also allows us to define more declarative UI applications.
It’s worth to mention all the analyses and conclusions described here are only theoretical. This article intends to challenge the way of developing applications and find new patterns that allow us to maintain applications with less effort and more flexibility.