1

I have been working on an application which consists of several cohesive services, Customer, Invoice, PurchaseOrder, Highlight, the application is deployed within a mono repo and each service is packaged within its own directory, in a package by feature arrangement.

The application is serverless and each entity (aside from highlight) is backed by a dynamo DB table and initially things were o.k, and the system was loosely coupled, functions are deployed as AWS lambda functions.

Soon requirements meant that for example the Invoice service needed data from the PurchaseOrder service and so the simplest method was to include in the lambda deployment for an Invoice the helper for PurchaseOrder and then query dynamo DB.

I have now almost finished the application and looking back, whilst I started out with good intentions of high cohesion and loose coupling as soon as the delivery pressure ramped up I began 'code sharing' across packages and have now got tight coupling across the application.

The highlight package is actually just lots of key values displayed on a dashboard such as number of unpaid invoices, number of purchase orders with less than certain percentage of invoices remaining unpaid etc, and so spans all of the logical services so this is the most tightly coupled package.

I am looking for some advice on how I could have countered this tight coupling so I can avoid it again in future. One way would be to call the API's across package to get the data I needed, so for example the Invoice functions could invoke, through API gateway endpoints within the PurchaseOrder package, but this would add additional latency and would also incur additional cost.

12
  • why didnt you make one service call another if it needed data?
    – Ewan
    Nov 8 '21 at 13:30
  • Well depends what you mean by one service call another? They technically do if talking at the code level, but not if you mean over http. The reason for not calling over http was because the endpoints didnt necessarily give me exactly what I needed whereas using the DAO's themselves did, so I deployed the DAO's rather than writing more http endpoints, this also coupled with the additional latency of calling services over http.
    – berimbolo
    Nov 8 '21 at 13:40
  • OK, lets take the http out of it, can't you just have a dll (or lib of some kind) for service X which you import into service Y ensuring that you only call interface methods and maintain loose coupling?
    – Ewan
    Nov 8 '21 at 13:50
  • I mean then its just a choice of whether you write your new function in one file or another. Choose the one that maintains loose coupling
    – Ewan
    Nov 8 '21 at 13:51
  • This is what I have done, but the thing that troubles me is the services are deployed independently as lambda functions and packaged with up their code, so for example service Y is deployed and it contains the modules necessary for service Y, but because I now call functionality from service X I have to deploy that code when it is packaged as a lambda. I think perhaps calling over http would be the only real way to avoid this coupling, or maybe the boundaries I defined for services are actually wrong because I have the need to do this.
    – berimbolo
    Nov 8 '21 at 14:19
3

You've run into an issue that is an ongoing careful balancing act in modern software development. The more you loosely couple your components, the less you can rely on performance optimizations that rely on more than one component at the same time.

For example, if you want to extract a report which links all your Customer, Invoice and PurchaseOrder data; the most performant method would be to write a single query and let your database server optimize its query. This is something a db server tends to specialize in.
However, from a coding perspective, this immediately muddies your loosely coupled components, as there is no clear separation between the three domain entities you've defined.

From a code structuring perspective, you'd rather fetch the Customer, Invoice and PurchaseOrder data separately, so that you can keep these components independent from one another. That's great for your code structure, but your query performance is going to suffer because of it.

I'm using repositories as the example here, but the same applies to your situation. You're stuck between the cleanly loosely coupled services, and a desires to optimize things by coupling your services.

Whether you value performance or code structure, is not something we can universally answer.

whilst I started out with good intentions of high cohesion and loose coupling as soon as the delivery pressure ramped up I began 'code sharing' across packages and have now got tight coupling across the application

There's a saying in my native language, which loosely translates as "the last stones seem to weigh the most".

That is the classic development story. This isn't even development specific, the same applies to most projects people tend to undertake, e.g. building a house, cleaning a house, or painting something.
In the beginning, they start with clean broad strokes, keeping things efficient and elegant. But at the end of the project, when the end is very much in sight and a minor hindrance is encountered, people are much more likely to sweep things under the rug and be done with them. No one wants to redo the foundation when they're already doing the final detail work.

Side fact: the house I live in is a monument to this kind of behavior. I didn't cause it, past owners did (I just rent it), but it very much amuses me as a software developer, and annoys me as the tenant.

This is human nature, and there are justifications for why we engage in this behavior - though you run the risk of regretting your decision down the line. Especially in software development, the unexpectedly long-lived nature of codebases tends to bite you back in the end, which is why good practice developers are so sensitive to bad practice.


The main takeaway here is that you can't realistically expect that you would plan for everything correctly in advance when you first laid the groundwork. Instead, you need to shift your expectation to adjusting the groundwork when the need for it being adjusted presents itself. This comes in many forms.

Clean coding practices help lighten the load when the time to adjust things comes.

But sometimes a late change requires a significant change to the codebase, and this is where you need a PO or team lead who values the clean code enough to dedicate the needed effort and time to it. More often than not, even with the cleanest coding practices, the latter is what fails and causes these kinds of issues, and as a developer you generally don't have the authority to override this. At best, you can hope to sway your PO/team lead/manager's mind and give you the needed time.

3
  • 1
    You have pretty much hit the nail on the head, the project has had a lack of product ownership until late in the development and we then had some conflicting requirements which both set us back and increased the pressure. I guess in hindsight I would possibly have looked to better define the domains\services of the application but Im sure we would have hit some other issue that I would now be asking about instead. Great response to me question, thanks.
    – berimbolo
    Nov 8 '21 at 15:47
  • 1
    @berimbolo: I'm all for trying to do things better from the first time round when you can; just make sure to avoid analysis paralysis. It's so very easy to overdo your initial prep work. Balancing YAGNI/agile on one hand and SOLID/abstraction on the other is something you really want to do, and not err blindly towards one. (Note that a particular company culture might push you to favor one over the other. That's fine as long as the company then also acknowledges and deals with cases where they erred the wrong way)
    – Flater
    Nov 8 '21 at 15:52
  • 3
    @berimbolo I'd just like to add something regarding cohesion/coupling; you've said "I started out with good intentions of high cohesion and loose coupling" - one thing to realize is that the first decomposition of the system is based on the limited information you had at the start of the project. It's not necessarily going to retain good cohesion and loose coupling even if you enforce the design, because those properties depend how well the structure supports actual change trends. The trick is notice when to reshuffle things to approach good cohesion / loose coupling over several iterations. Nov 8 '21 at 19:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.