How to survive with internal library
This is the story of how I started to work on a small library that was used within the company for implementing the same feature in different domains. Let’s name it Address Book. Address Book was meant to be a separate module, since the simplicity of its definition and easiness of its isolation. But more than that, since we had different products placed in different repositories, we needed to place Address Book in the third repo to be able to use it in both domains.
So was it. The separate isolated library with its own approaches, tests (no), and a couple of guys how came here from time to time to add or comment out some feature.
Then the new domain was opened, who also needed the book, but what’s important there, it needed not to address but telephone book (in fact all those domains needed other feature, but I decided to rename it). The telephone book looked really similar to the address book. Same catalog, but gives works with not addresses, but phones. Yet, they still use the same core service for giving contacts, which are consisted of addresses and phones.
And this new domain provided their own solution regarding the telephone book, because they didn’t know about the solution of first two domains, and because nobody knew back then that all domains will be a part of the same super app.
The story of how I merged to approaches
So what I had in the beginning:
- A library which represented address book for the first two domains, which implemented one set of features and created some kind of wrapper to work with 3rd party service
- A module that represented a telephone book for the third domain (placed in its codebase), which implemented another set of features and also had its own wrapper to work with 3rd party service.
Since the second module was written by me, deep inside I wanted to throw the first approach and take my module and evolve it into something beautiful :) But after some meetings and discussion, managers & engineers decided that it will a good impact on the future if we will merge those two approaches into one. A good impact, because we will have a single source of working with contacts, which will wrap the 3rd party service, and will present everything: telephones & addresses.
What did that mean to me? O month of pain while trying to do the really simple steps (on a paper simple steps):
- The library was split into two libraries: one for UI components and one for everything else. So I decided to put the first library into the second one as a separate module. (because if we will put every module as a separate library in the separate repository we will go nuts, everywhere should be a balance)
- The Telephone book module was not fully isolated from the project, so I needed to isolate it first
- This module also contained the work with some handy utils we had only on this project. So I had to decide what to do with that. To not duplicate everything I spoke with another team and we agreed to create a library for handy utils, so I placed everything there. Which also took some time, because when you know that your code will be used not only by your team but possibly everybody in the company, you start to filter out more.
- After that clean up I needed to move the isolated module into the library.
- And finally, I needed to integrate that library with the 3rd domain, instead of the old module, which I moved previously.
Just a beginning
The point is that it took so long, but still it was just the first big step to merge everything. And to be honest the second step had a distance of eternity. The step of refactoring and merging cores together, some internal approaches together. Step on which nobody had time because everybody needed new features.
So I decided to do that with small incremental steps. Every time I receive a new feature to build, I took a little bit more time to think about what I can clean up in the code base, what I can improve a bit. And at some point library will be refactored. As Uncle Bob said:
THE BOY SCOUTS HAVE A RULE: “Always leave the campground cleaner than you found it.” If you find a mess on the ground, you clean it up regardless of who might have made it. You intentionally improve the environment for the next group of campers. (Actually, the original form of that rule, written by Robert Stephenson Smyth Baden-Powell, the father of scouting, was “Try and leave this world a little better than you found it.”)
Since I completely agreed with that postulate, I decided to do it. In fact, I’m still doing this because small incremental steps take a tremendous amount of time globally, but they are not noticeable in terms of affecting the scope of work for the new features or bugfixes.
Refactoring is a must… but painful
The main pain in the refactoring of the library, that everything which is public is the potential component used by the client of the library. Thus, if you want to change it, you must make it deprecated and put a new approach near. Thankfully in my case, I have no tonnes of the users, I have only 3 and all of them are domains of the main big super-app project. Which means, I can clone all 3 repositories and check, what functionality they used. I know that seems a bit of a hack, but we should utilize everything we have and provide the solutions attached to our situation.
To help myself with the refactoring of the library and not lose my mind while trying to connect everything together and understand the situation, I’ve done three things, which really helped:
- If something is stale, but public and used by clients, deprecate it and write in the description, what should be used instead.
- I created a special annotation that indicated the version from which some method appeared, which helps me to remember, in what time what was added.
- What was not used outside the module or by the clients of the library, I marked as
With visibility, everything is not so smooth, because if you have a single module library,
internal is a way of solving things, but if you have a multimodule project and all modules are published, then you will have public classes/methods because you use them in the root module of your library.
The only solution I found right now, is propagating in the documentation to use only dependencies, which represent the public API of the library, and don’t use anything else. I’m sure there is a more strict solution (
@RestrictTo didn’t help btw, I think because it is tied to google support libraries). But I didn’t find it yet.
When different users use different approaches
Actually, the library itself consists of two parts:
- The core which is the wrapper on a 3rd party Contact book library
- The activities/fragments that are written in the library to work with this core and provide some UI of working with address and phone contacts, so the user of the library, will just run the activity and the rest will be done inside
So in an ideal way, the interaction with clients of the library should be minified. Unfortunately, it is not possible entirely because the Contact book has to be initialized and prepared, given some conditions, and for example for different cities, there may be different Contact books, which means then if you switching the city the old book can be flushed and new one need to be fetched. The point is the part of the core will be part of the public API of the library.
The main problem here is next: different clients use different approaches to work with concurrency. Some clients use Rx, some clients use coroutines. And part of the library which was written a long time ago sharpened for Rx, and the other part which was merged in used coroutines. In that case, as the first step, it is better to use good old callbacks, and then provide the two wrappers: one for Rx and one for Coroutines.
Concentration is a key
Another problem of working with such libraries (which have different approaches, a lot of stale code, and several clients) is the concentration. I know that it may sound stupid, but when you work on the common feature in your project, in most cases architecture is defined, the utility methods are here, colleagues are here to help if something is not familiar.
But here the situation is different. No architecture, since the library uses different approaches. No common solutions, since the library uses different approaches. No help, because the guys who worked on the library, already or not working in the company or working in some other areas and they could forget some things. No time for refactoring, because how a manager thinks: “If we merge two libraries into one, we will have features of both, that we can easily use”, which is not true, but still no time.
And now you try to gather up in your head all possible usages of the thing, you want to refactor, remembering how those and those things can be used in the different client, it is very very important to be not distracted.
Why do you need backward compatibility?
Since the library is used by 3 clients, you may ask “Why do you need backward compatibility”, just release a new version whatever you like, put changelogs and announce that to the other teams. Whenever they want, they will migrate to the new version.
That’s true in the situation when all clients are separate applications. But when all clients are the libraries for the one big super-app application, that’s not the case. Because when everything will be built the only one version of the library will be chosen and put as a version for everyone. Now if one domain uses the method from the old version of the library and in the new version this method is removed, or declaration is changes, that domain will have problems in runtime, since internally they received a new version of the library, even if they didn’t want to.
Since not everybody has the capacity to migrate to the new version of the library right away (but later it can be planned), it is important to provide backward compatibility.
I use one trick to recheck that everything is compatible. I run the clients who have to use the old version for now and try to compile them with a new version. If everything is compiled, then I didn’t break the backward compatibility in our small group of library users (though I could break it generally).
Sometimes it can lead to funny moments when you have a deprecated interface which just extends the new interface that should be used from the new versions like:
message = "Use CompleteCallback instead of this. ResultCallback will be removed",
replaceWith = ReplaceWith("CompleteCallback", "com.careem.chat.core.models.CompleteCallback")
interface ResultCallback : CompleteCallback
Working not inside the isolated project, but the project, which is used by other engineers, can add new experience and can be really challenging, but at the same time it's really interesting.
If you liked that article, don’t forget to support me by clapping and if you have any questions, comment me and let’s have a discussion. Happy coding!
Also, there are other articles, that can be interesting: