How to Organize the Packages of your Project?

You start with a few classes, everything seems manageable, and then suddenly, your once-simple project resembles an overgrown forest where finding a file feels like searching for your keys in the dark. Should you organize by feature, group things by layers, or just give up and move to a cabin in the woods?

In this article, I’ll dig into these organizational strategies, break down their pros and cons, and settle the debate (or at least try to). So, grab your favorite debugging snack and let’s bring some order to the chaos.

Packages by Features

Let’s take a website selling cars online. Imagine you’re building this e-commerce platform where users can browse, compare prices, and check stock levels. Now, instead of dumping every class into a folder called com.project.megamess, you organize by feature. This isn’t just tidy, it’s sanity-saving.

Here’s how the package structure might look:

com.carsforsale
├── cars
│   ├── CarController.java
│   ├── CarService.java
│   ├── CarRepository.java
│   └── CarMapper.java
│
├── prices
│   ├── PriceController.java
│   ├── PriceService.java
│   ├── PriceRepository.java
│   └── PriceMapper.java
│
├── stocks
│   ├── StockController.java
│   ├── StockService.java
│   ├── StockRepository.java
│   └── StockMapper.java
│
└── users
    ├── UserController.java
    ├── UserService.java
    ├── UserRepository.java
    └── UserMapper.java

What advantages does the organization by feature has?

  1. Code Readability: Instead of your teammate going on a wild goose chase between controllers, services, and repositories packages, they know that everything related to the Cars feature is inside the cars package.
  2. Scalability: Want to add a new feature for user reviews? Easy. Just create a reviews package, add your controllers, services, mappers, and repositories.
  3. Maintenance: Debugging becomes less of a nightmare when you don’t have to hop between five different packages to figure out why the CarService isn’t talking to the StockRepository. It’s all there, side by side.

Inside each feature package, you have:

  • Controllers for handling HTTP requests;
  • Services for your business logic;
  • Repositories for database interactions;
  • Mappers to convert entities into DTOs.

Packages by Layers

Imagine you’re still working on that website selling cars online. Your codebase needs to handle user interactions, manage business logic, access databases, and map entities for data transfer. With a layer-based organization, you’re not grouping by feature like cars or prices; instead, you create packages based on their roles. Here’s what that might look like:

com.carsforsale
├── controllers
│   ├── CarController.java
│   ├── PriceController.java
│   ├── StockController.java
│   └── UserController.java
│
├── services
│   ├── CarService.java
│   ├── PriceService.java
│   ├── StockService.java
│   └── UserService.java
│
├── repositories
│   ├── CarRepository.java
│   ├── PriceRepository.java
│   ├── StockRepository.java
│   └── UserRepository.java
│
└── mappers
    ├── CarMapper.java
    ├── PriceMapper.java
    ├── StockMapper.java
    └── UserMapper.java

What are the advantages of separating the packages by layers?

  1. Clarity in Responsibilities: Each layer knows its job and sticks to it. The controllers handle HTTP requests, the services process business logic, repositories deal with database operations, and mappers convert entities to DTOs and back. Simple, right?
  2. Easier Onboarding: New developers can quickly understand the project structure. “Where’s the logic for updating car prices?” – check services. “Where’s that database query for user data?” – in repositories.
  3. Common Ground: This is the go-to structure for many traditional Java projects, so it’s often familiar territory.

What about the disadvantages?

  1. Feature Separation Becomes Murky: Sure, the structure is clean in theory, but when a new feature comes in (say, vehicle promotions), it spreads across your controllers, services, and repositories like a tangled spiderweb. Navigating all these layers to update or debug a single feature can feel like a game of “Where’s Waldo?” but with more frustration.
  2. Cross-Layer Choreography: Let’s say CarController calls CarService, which calls CarRepository, which uses CarMapper. You end up jumping back and forth between packages.
  3. Package Bloat: The bigger your project gets, the more crowded each layer becomes. Your services package might end up resembling an overstuffed closet where CarService.java is wedged between UserService.java and PriceService.java, making it difficult to see the organization’s real value.

So, why do people still use it?

Layer-based package organization is the tried-and-true approach when:

  • Standardization is key for teams used to traditional Java setups.
  • Layer-focused refactoring or improvements are part of your project’s regular process (e.g., revamping all repositories for a new database migration).

Where to Put Common Classes and Configurations

Every Java project has its share of common classes that don’t belong to a single feature or specific layer: configuration files for security, web, CORS, filters, and other setup essentials. These classes are like the utility belt in a superhero’s arsenal: essential, versatile, and a little bit all over the place if not organized correctly. So, where do these classes live in a feature-based system versus a layer-based system? Let’s take a look.

Feature-Based Organization

In a project structured by features, common classes can be a bit tricky to place since they don’t fit neatly into any single feature. Here’s the approach that works best:

com.carsforsale
├── common
│   └── config
│       ├── SecurityConfig.java
│       ├── WebConfig.java
│       ├── CorsConfig.java
│       └── FiltersConfig.java
├── cars
│   ├── CarController.java
│   ├── CarService.java
│   ├── CarRepository.java
│   └── CarMapper.java
│
... (other feature packages)

  • Why This Works: It centralizes configurations that apply to the entire project. By placing them in a common.config package, you separate cross-cutting concerns from feature-specific code, making it easier to manage and find them.
  • Trade-offs: You’ll need to establish conventions for what goes into common.config versus other potential shared directories. It may feel a bit detached from the rest of your application’s structure, but it keeps things uncluttered.

Layer-Based Organization

In a project organized by layers, finding a home for common configuration classes feels a bit more natural. Here’s how that might look:

com.carsforsale
├── config
│   ├── SecurityConfig.java
│   ├── WebConfig.java
│   ├── CorsConfig.java
│   └── FiltersConfig.java
│
├── controllers
│   ├── CarController.java
│   ├── PriceController.java
│   └── UserController.java
│
├── services
│   ├── CarService.java
│   ├── PriceService.java
│   └── UserService.java
│
├── repositories
│   ├── CarRepository.java
│   ├── PriceRepository.java
│   └── UserRepository.java
│
└── mappers
    ├── CarMapper.java
    └── UserMapper.java

  • Why This Works: Since the layer-based structure already separates your code by function, a top-level config package aligns with that logic. The configuration classes don’t belong to any one layer, so having them in a top-level config package makes sense.
  • Trade-offs: This setup keeps your configuration isolated but might add another package at the top level, which can make your project structure appear broader. However, it’s still clear and easy to navigate.

Comparing Feature-Based and Layer-Based Organization

So, you’ve looked at organizing your Java project by features and by layers. Now comes the real question: which one is better? Of course, this isn’t a simple Coke vs. Pepsi showdown. Both structures have strengths and weaknesses, and your choice will depend on the nature of your project and your team’s needs. Let’s break down the comparison.

1. Visibility and Maintainability

  • Feature-Based Organization:
    • Pros: High visibility for feature-centric development. If you need to find everything related to “cars,” you go straight to the com.carsforsale.cars package and find all related classes in one place. This can be a huge win for projects with a strong feature focus or when onboarding new team members.
    • Cons: Can become less visible when dealing with common logic shared between features, as that will need to be placed in separate common or shared packages.
  • Layer-Based Organization:
    • Pros: Easy to find classes based on their role. Need to see all controllers? They’re in com.carsforsale.controllers. This structure is predictable and familiar, especially for teams with experience in traditional Java setups.
    • Cons: Lower visibility for feature cohesion. Finding all classes related to a specific feature like “cars” means jumping between packages, potentially causing more cognitive load when navigating the code.

Verdict: For visibility specific to features, feature-based organization wins. For visibility based on role, layer-based organization is more straightforward.

2. Number of Classes Per Package

  • Feature-Based Organization:
    • Pros: Packages are organized by business logic, so they contain a reasonable number of classes related to each feature. This approach often scales better in terms of splitting up classes, preventing massive single-package bloat.
    • Cons: As you add more cross-feature components (e.g., common or utils packages), it might seem like a few outlier packages have lots of unrelated code.
  • Layer-Based Organization:
    • Pros: Each layer is neatly organized, so your repositories, services, controllers, and mappers are compartmentalized. This can initially make each package look smaller.
    • Cons: Each package might get bloated over time, especially in large projects. As you add more services or controllers, those specific layer packages can become cluttered and harder to navigate.

Verdict: For smaller, more organized packages, feature-based organization has the edge. Layer-based organization can start clean but tends to accumulate more classes in each package as the project scales.

3. Best for Small vs. Big Projects

  • Feature-Based Organization:
    • Small Projects: Works well, especially if the project is feature-centric with a clear domain. You can organize the code from day one with a focus on features without adding much complexity.
    • Big Projects: Excellent for large codebases. It promotes modularization and makes it easier to break the project down into smaller, self-contained features. This also allows parallel development since teams can work on different feature packages without stepping on each other’s toes.
  • Layer-Based Organization:
    • Small Projects: Also a good fit, as the initial codebase might not have many layers, making it easy to maintain. The straightforward nature of having controllers, services, and repositories in their own spaces can keep things clear in simple apps.
    • Big Projects: This is where it can become unwieldy. As more classes are added to each layer, navigating and maintaining these packages becomes harder. Dependency management across layers can lead to tight coupling, especially when cross-layer interactions aren’t carefully planned.

Verdict: For large projects, feature-based organization wins due to better scalability and maintainability. For small projects, either approach can work well, but layer-based organization might feel simpler to start with.

4. Unit Testing Ease

  • Feature-Based Organization:
    • Pros: Since classes related to the same feature are grouped together, writing unit tests that cover an entire feature’s behavior is more straightforward. Test classes can be placed in corresponding test packages that mirror the structure of the main code, making tests easy to locate.
    • Cons: It can be more challenging to share test utilities if they’re buried in separate feature-specific test packages. You might need a shared test utility package to keep things organized.
  • Layer-Based Organization:
    • Pros: It’s simple to create test packages that reflect the structure of your controllers, services, repositories, etc. This approach can make testing individual units (e.g., testing all service classes) more systematic.
    • Cons: Writing integration tests for end-to-end feature behavior can be less intuitive. Since feature-related classes are split across packages, ensuring test coverage for an entire feature means pulling from different test directories.

Verdict: For unit testing that focuses on the end-to-end behavior of features, feature-based organization is better.

Conclusion

  • Feature-Based Organization: Ideal for large projects where modularization is key and for teams that need high visibility into feature-specific code. It scales well and makes feature development, unit testing, and onboarding easier.
  • Layer-Based Organization: A good fit for smaller projects or when the team prefers a traditional structure with a clear division of responsibilities by function. It’s familiar, structured, and can be easier to maintain for projects that aren’t overly complex.

Ultimately, the choice between feature-based and layer-based organization comes down to your team’s needs, the scale of your project, and the type of development work you expect to do. Just remember: code organization isn’t set in stone. As your project evolves, so can your approach: just try to avoid reorganizing the whole codebase on a Friday afternoon. Your future self will thank you.


Never Miss Another Tech Innovation

Concrete insights and actionable resources delivered straight to your inbox to boost your developer career.

My New ebook, Best Practices To Create A Backend With Spring Boot 3, is available now.

Best practices to create a backend with Spring Boot 3

Leave a comment

Discover more from The Dev World - Sergio Lema

Subscribe now to keep reading and get access to the full archive.

Continue reading