5 Most Popular Package Structures for Software Projects

The structure of the top-level packages in your codebase is one of the most important aspects of your project’s architecture. In this article, I’ll review several common package structures and discuss their respective benefits and drawbacks.

Clean Architecture

“Clean Architecture” is a term that had been popularized by Robert (Uncle Bob) Martin. 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 the 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

I get it, it’s important to package my application properly, but what does “properly” mean here? In the following sections, I’ll describe the most popular approaches to packaging your code.

Unstructured Packaging

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” or, worse, legacy names that didn’t keep up with the project’s evolution, so they’re lying.

The downsides of this approach is that you’ll end up with huge packages that will contain tens, or even hundreds of unrelated classes. The names of the packages won’t reflect the application’s functionality, 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. New developers will have really hard time ramping up on a codebase where the classes are found in seemingly random packages.

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 means that you bundle together entities that have the same type. Let me show you some examples of top-level packages to clarify what I mean:

Type = language constructs:

  • classes
  • interfaces
  • enums

Type = framework constructs:

  • activities
  • fragments
  • services

Type = functional constructs:

  • controllers
  • views
  • usecases

The benefit of packaging by type is that it’s a simple strategy that doesn’t require much effort.

The downside is that it still results in a challenging structure 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 you’ll need to be constantly jumping between them when working on a single piece of functionality.

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:

  • ui
  • esb
  • database
  • networking

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 your restrictions are violated and even write static automated checks to detect these 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 will not help you much. In addition, separation into 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.

The name of this strategy implies that it’s applicable to GUI applications exclusively, but 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 endpoints 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:

  • authentication
  • productdetails
  • merchantslist
  • cart

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 your app is packaged by screen, you can immediately ignore most of the packages in the codebase when you investigate bugs.

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 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 projects use Package by Screen, but call it Package by Feature. The idea is that screens are “features” from product’s perspective, so you’ll get “feature requests” in the form of new screens (or changes to existing ones). It’s important to realize that, fundamentally, screens (and other types of IO ports) 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 from the 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:

  • authentication
  • users
  • products
  • offers
  • checkout

For a media player, you might have something along these lines:

  • albums
  • tracks
  • artist
  • collections

For a payment service app, you’ll most probably have these features:

  • authentication
  • users
  • accounts
  • transactions
  • receipts

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 do we place the code that implements 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 very useful. For example, when I jump into 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 the 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.

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.

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 discover that some of your prior understanding was incomplete. Therefore, you’ll need to be constantly investing effort into making sure that your project’s package structure is in sync with your up-to-date understanding of the business domain. In other words: routine refactorings.

Said that, in my opinion, the above “downside” is not really a downside becaues you’ll need to learn the business domain anyway. If you don’t, chances are that your codebase will be littered with wrong abstractions, hacks and unreasonable coupling. Therefore, if your packaging strategy forces you to think about the most fundamental abstractions of your business domain, it counts as a big benefit in my book.

Conclusion

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 your app’s architecture.

I, personally, always recommend the Package by Feature approach. Package by Screen is largely alright, but it’s more cumbersome and less explicit.

That’s all for this time. As always, thanks for reading and don’t forget to subscribe to my newsletter if you liked this article.

Check out my premium

Android Development Courses

8 comments on "5 Most Popular Package Structures for Software Projects"

  1. In Package by Type section, why are views under functional constructs? Shouldn’t views come under framework constructs as View is android’s framework like Activity and Fragment?
    Also, what’s esb?

    Reply
    • Hi Anirudh,
      Views aren’t necessarily Activities and Fragments. In fact, that’s one of the biggest mistakes promoted by the official guidelines. You can read this article to get more info.
      ESB = enterprise service bus. It’s like event bus on steroids.

      Reply
      • I didn’t mean that activities and fragments are UI elements. When activities, fragments and services come under framework constructs, why can’t views be included in it? A view is an android framework. And how do views come under functional constructs?

        Reply
        • Sorry, I misunderstood your question.
          Sure, if you’d use “package by type” approach and would have Activities, Fragments, etc. top-level packages, then it would make total sense to have Views top-level package for Android View classes as well. in fact, that’s something I see quite often.
          Again, not something I’d recommend to do.

          Reply
  2. Hello, so when you say `Package by Feature` is the Holy Grail, does this suggest that it is your personal favourite? I am leaning this direction with a frontend microservice approach, each feature will be developed by it’s own squad and have a dedicated repo. There would be common features and UI components shared between the product features and all managed in an open-source way, each squad can contribute back to the main UI/lib branch etc.

    Reply
    • Hey,
      Yes, I use package-by-feature whenever possible. Never worked on frontend microservices, but it sounds like package-by-screen might also be a viable approach in that case (if each microservice corresponds to specific screen(s)).

      Reply
  3. Hey,

    Your article is great every time I come to learn something architectural and technical your posts always fulfill my appetite to my thinking level. I am curious about your guidelines towards the strategies to design the architecture. I always have difficulty to learn the connection between entities and come up with a suitable architecture. It would be great if you can shed light on this issue at the beginning level because this is something that is of high importance.

    Thanks and regards!

    Reply

Leave a Comment