In this post I want to discuss one of the most important aspects of your project’s architecture: package structure.
Surprised? You’re not alone, believe me. Many developers don’t consider the structure of packages to be of high importance, let alone core part of application’s architecture. However, as you’ll learn from this article, this couldn’t be farther from truth.
I myself had been ignorant about the importance of package structure for a long time. Basically, until I started listening to Robert Martin’s (aka Uncle Bob) talks and reading his blog. It is only then that the full significance of this aspect became evident to me.
You might’ve heard about “Clean Architecture”. This term had been popularized by Robert Martin, and he even wrote a book bearing the same title. According to Uncle Bob, one of the core components of a clean architecture is the structure of top-level packages:
So what does the architecture of your application scream? When you look at the top-level directory structure, and the source files in the highest level package; do they scream: Health Care System, or Accounting System, or Inventory Management System? Or do they scream: Rails, or Spring/Hibernate, or ASP? Robert Martin
However, even after I learned how to structure my packages properly, I still couldn’t fully appreciate the benefits of such a structure. See, when you write application from scratch, even as part of a team, you’ll usually be able to navigate the codebase and find what you need regardless of how the packages are structured. At least, that will be the case for some time, until the application gets too big to fit comfortably within your brain’s index. Therefore, even if you don’t invest any effort into this aspect, you won’t feel the pain.
It’s only when you need to ramp-up on an existing codebase, or you get back to your project after six months of not touching it, that you notice the difference. And boy, there is a huge difference. Jumping into properly packaged code feels like meeting an old friend: several minutes of awkwardness, and then it’s like you’ve never gotten out of touch. Jumping into unstructured application, on the other hand, feels like having a conversation with a drunk weirdo at a party you didn’t want to attend in the first place. It’s not cool.
So, it’s important to package your application properly, but what does “properly” mean here? In the following sections, I’ll describe the most popular approaches for packaging and discuss their benefits and drawbacks.
By the way, as you might see from Uncle Bob’s quote above, it’s the structure of top-level packages which is of utmost importance. However, it’s not to say that if you get that right, then you’re done. Not at all. Your goal is to build a tree of reasonably-sized packages inside your app and make sure that the edges between nodes tell the story of your app’s architecture all the way down to tree’s leaves. However, it’s the structure of top-level packages that is most important and that’s the topic of this article.
That’s not really a packaging strategy, but a lack thereof. In some cases, it manifests itself as having just one package for all the code. In other cases, you’ll see random generic packages like “v1”, which don’t help you understand the application in any way.
The downsides of this approach is that you’ll end up with huge packages that will contain tens, or even hundreds unrelated classes. The names of the packages won’t be related to application’s functionality in any way, so good luck finding what you need. The general mess associated with unstructured package structure will increase your mental overhead and will bother you each time you touch the code. You’ll end up being distracted and less effective. In extreme cases, the ugliness of the project might invoke some sort of immune reaction from your brain.
Don’t do it to yourself and others.
Package by Type
This is the simplest and the most intuitive packaging approach. Most new developers naturally converge on this structure, even if not taught explicitly.
Package by type basically means that you put together entities that have the same type. The notion of “type”, however, can refer entities from different families. Sounds complex, but it isn’t. Let me show you some examples of top-level packages to clarify what I mean.
Type = language constructs:
Type = framework constructs:
Type = functional constructs:
The benefit of packaging by type is that it’s a simple strategy that doesn’t require much effort.
The downside is that it doesn’t help you much and results in a mess once your project grows in size. For example, even if your code is perfect, top-level “controllers” package with 50 controllers in it isn’t helpful beyond suggesting that you have controllers in your application (which isn’t a lot). In addition, individual features of your app will be distributed among multiple top-level packages, so it’ll be difficult to work with and maintain.
As I wrote above, package by type feels natural when you just start your career in software. Many new developers use it. I too used this strategy in the past, as is evident from my very first application. “Controllers”, “mvcviews”, “services” are all designations of different types.
Package by Layer
A slight variation on Package by Type is Package by Layer. This strategy suggests that you put entities belonging to the same logical layer together. The resulting structure of top-level packages might look like this:
The benefit of this strategy is that it’s simple and actually a bit more useful than Package by Type. For example, you might want the dependencies between two logical layers to go in just one direction, or, maybe, you want these layers to not have direct inter-dependencies at all. With Package by Layer you can know when these restrictions are violated and even write static automated checks to detect potential violations.
The downsides of this strategy is that the information it conveys is still not that interesting in most cases. When you look at a codebase, knowing that there is a database, or UI in there is not the information you’d like to be equipped with. In addition, segregation according to layers is still very crude, so it’s only a question of time until these top-level packages will become huge.
Package by Screen
Now we finally reach the packaging strategies that experienced developers use and love.
The first one is Package by Screen. Judging by its name, this strategy sounds applicable to GUI applications exclusively. However, that’s not exactly the case. See, “screen” is just one specific realization of IO port used in GUI system. But there are other IO mechanisms out there as well (e.g. REST endpoint in backend, subcommand in command-line tool, etc.). Therefore, the generalization of Package by Screen to non-GUI systems would probably be called Package by IO Port.
When you use this strategy, top-level packages in your project will correspond to individual screens, or closely related groups of screens:
The benefits of this packaging scheme is that the names of your top-level packages mirror the actual structure of your application. This makes it much easier to navigate the codebase and spares a lot of effort in the long run.
For example, when you get a bug report, in most cases it will specify on which screen the bug occurred and how it manifested itself. That’s only reasonable when you realize that testers and users interact with the system through its IO ports. If you’ve got your app packaged by screen, it’ll allow you to immediately rule out and ignore all the unrelated logic in the app when you investigate bugs (well, at least initially).
Another benefit of packaging by screen is that this approach scales well. As your application grows in size, you naturally add new top-level packages corresponding to new screens. Therefore, you’re pretty much guaranteed that you won’t have monstrous packages with tens or hundreds of classes in your project.
As far as I know, there are no major downsides to this approach. However, in my opinion, you can still do better than that using Package by Feature strategy discussed in the next section.
Note: many developers use Package by Screen, but call it Package by Feature. That’s a widespread confusion because screens are “features” from product manager’s perspective, so you’ll get “feature requests” in the form of new screens (or changes to existing ones). However, screens (or any other type of IO ports for that matter) aren’t your app’s features. They’re just the means of exposing the features to the outside world. Therefore, Package by Screen isn’t Package by Feature.
Package by Feature
And now to the holy grail of packaging strategies: Package by Feature.
This strategy basically says that the top-level packages of your application should correspond to the core concepts in app’s business domain. Let me demonstrate the general idea with several examples.
If you work on e-commerce platform, these are some of your features:
For a media player, you might have something along these lines:
For a payment service app, you’ll most probably have these features:
As you see, features like “user” and “authentication” are quite common. That’s only natural given the way most services work. Other features, like “checkout”, are fingerprints of specific business domains.
One important point to understand here is that none of the aforementioned packages contains screens-related logic. Login screen is not part of authentication feature, for example. Where are the screens then? Well, I usually have a dedicated top-level package called “screens” which contains subpackages structured according to Package by Screen approach. This prevents the pollution of top-level package structure with less important details, while still keeping all the benefits of Package by Screen in the codebase.
That’s how I structured the top-level packages in one complex application that I wrote:
Note the “screens” package. That’s where all the presentation layer logic resides.
Another interesting package there is “common”. That’s basically the package that aggregates general functionality which isn’t related to any specific domain concept:
The benefit of this strategy is that you have an explicit picture of your entire business domain. That’s super important and useful. For example, when I join new existing projects, I usually don’t care about their screens. It’s just irrelevant information initially, and, anyway, I will probably not touch more than a handful of them anyway. However, I care a lot about the architecture of this application and what it does. Having top-level packages like “user, “product”, “charts”, “transactions”, etc. immediately gives me a general idea about the breadth and the complexity of app’s domain. Sure, it’s not all the info you need to start working on a project, but it’s the best start you can get.
In addition, structuring top-level packages to mirror the business domain helps you think. For example, when you need to implement new features, you immediately get into the mode of “let’s understand which business domains will be involved and how they’ll interact”. When you deal with relatively small and simple applications, this question is not that important and you can start building new screens right away. However, when you work on big and complex projects, adding or changing UI will often be the easiest part. Figuring out where to get the required data from, which algorithms to reuse, which domain concepts need to change – that will be the main part of your job.
Yet another benefit is that since you put just the domain logic into your top-level packages, without screens, it’ll be easier to make their contents unit testable.
And specifically in the context of Android, package by feature allows you to avoid preliminary modularization and be prepared to modularize your project at any time. Furthermore, the modules that you’ll extract from your feature packages will not contain screen-related Android classes like Activities and Fragment, thus making inter-module dependency injection much simpler. In fact, you can even make your domain packages independent of Android framework at all, in which case you’ll be able to extract them as plain Java or Kotlin library modules.
There are more benefits to package by feature, but I guess I’ll stop here. Hopefully you got the idea.
The only downside of this approach that I’m aware of is that it takes effort to implement and maintain. See, distilling your business domain concepts and understanding the relationships between them takes a lot of effort. That’s the real hardcore architecture, right there. In addition, as your application grows and evolves, you’ll be learning more about your business domain and will discover new concept and inter-dependencies. So you’ll need to constantly invest effort to make sure that your most complete view of business domain is reflected in the package structure. In other words, you set yourself up for constant refactoring. At least, at the initial stages of the project.
However, in my opinion, the above is not really a downside, but actually a benefit. You’ll need to learn about your business domain anyway and if you’ll get lazy about that, chances are that your codebase will be littered with wrong abstractions, hacks and unreasonable coupling. That’s not my theoretical assumption, I saw these outcomes in real projects and I even observed how they accumulate over time. Therefore, if your packaging strategy forces you to think about the most fundamental abstractions of your business domain, it’s absolutely great in my opinion.
In this post I covered the most popular strategies for packaging software projects. I hope that the discussion of their respective benefits and drawbacks helped you to see that the structure of your top-level packages is one of the most important parts of app’s architecture. It’s just impossible to overstate the difference between projects that implemented good packaging strategy and projects that didn’t.
I, personally, always recommend to use Package by Feature approach. Package by Screen is largely alright, but it’s more cumbersome and less explicit.
As I already wrote in one of my earlier posts, adopting proper packaging approach is one of the most important decisions on a new project. It’s also one of the first concepts that I try to explain to developers that I work with. If you ever need to refactor your codebase and it doesn’t have clear and useful package structure yet, start with that and watch how the task of identifying domain concepts and inter-dependencies between them leads your refactoring effort. It’s an amazing experience.
That’s all for this time. As always, thanks for reading and don’t forget to subscribe to my newsletter if you’d like to be notified about new posts.