Package by feature vs by layer
On This Lesson 11 topics
Lesson Overview
You now know how to create packages and hide classes inside them. One question remains, and it is a design question rather than a syntax question: which classes go together in one package? Two answers dominate Java projects.
- By layer (by technical type): all the "service" classes together, all the "repository" classes together, and so on.
- By feature: everything for one business feature together: the account classes in one package, the customer classes in another.
In the filing cabinet picture, this is the difference between sorting your papers by kind of paper (all invoices in one drawer, all letters in another) or by customer (everything about Sara in one drawer). Both work. They differ in what is easy to find, easy to change, and easy to hide.
Why this matters: the choice decides whether package-private can do its job. This lesson builds on the last one and prepares the demo lesson and the Bank Accounts v5 lab, where you reorganize a project by feature.
You will see class names such as OrderController, OrderService and OrderRepository. They come from layered web applications that you will build later in the roadmap. For this lesson, treat them as ordinary class names: a controller receives requests, a service holds business rules, a repository reads and writes stored data.
Learning Objectives
By the end of this lesson you will be able to:
- Describe package by layer and package by feature, and draw each folder tree.
- Group a list of classes into feature packages.
- Explain why layer packages force many classes to be
public. - Explain why feature packages allow package-private hiding.
- Read import lines to see how packages depend on each other.
- Give a short justification for choosing feature packages in a small or medium application.
- Name an honest trade-off of the feature approach.
Written Explanation
Package by layer
A layer is a technical role. Package by layer creates one package per role:
shop/
controller/ OrderController UserController
service/ OrderService UserService
repository/ OrderRepository UserRepositoryThis feels tidy: all repositories sit together. Many tutorials start this way. But look at what one change needs. Adding a discount rule to orders touches OrderController, OrderService and OrderRepository: three files in three packages. And the package names (controller, service, repository) say nothing about what the application is for.
Package by feature
A feature is something the business cares about: orders, users, accounts. Package by feature creates one package per feature and puts everything for it inside:
shop/
order/ OrderController OrderService OrderRepository
user/ UserController UserService UserRepositoryOpen the folder order/ and you see all that orders need. Deleting a feature means deleting a folder. A new teammate can find the order code by name. Package names now describe the business, which is why this is also called "screaming architecture" by some authors: the structure shouts what the app does. (We cite no source for that nickname; treat it as a side remark.)
The key difference: package-private
Package-private only helps when the classes that should share a secret are in the same package. Compare the same two classes in both layouts.
In a layered layout, OrderService (package shop.service) uses OrderRepository (package shop.repository). They are in different packages, so OrderRepository must be public. The compiler leaves no choice:
error: OrderRepository is not public in shop.repository; cannot be accessed from outside packageOnce it is public, anyone can use it, including a controller that skips the service and its business rules. In our test, a Main in a third package called new OrderRepository().find(7) directly and it worked.
In a feature layout, OrderService and OrderRepository are both in shop.order. The repository can be package-private. Code outside the package can use OrderService, the public front desk, and nothing else:
error: OrderRepository is not public in shop.order; cannot be accessed from outside packageThe rule of thumb for this course: layer packages force many classes to be public so the layers can reach each other, while feature packages keep a feature's helpers private to it.
What crosses a boundary
Features are not islands. An account needs to know its customer, and sends notifications. The imports show these links:
package bank.account;
import bank.customer.Customer;
import bank.notification.Notifier;Each import of another feature's class is a dependency, and each depends on a public class. Design the packages so that:
- the public surface of every feature is small (a service class and a few types others must see, such as
Customer); - dependencies point one way where possible:
accountusescustomerandnotification, butcustomerdoes not useaccount. Circular dependencies between packages make them hard to separate later.
If you need a class in two features, ask whether it belongs to one of them (the more fundamental one), or whether a third small package should hold it.
Is layering wrong?
No. Layers are still a real idea: controllers should not contain business rules, and services should not write SQL. The question is only how you group files into packages. You can keep layering inside a feature: order may hold a controller, a service and a repository, each doing its own layer's job. Both styles are used in industry. For small and medium applications, feature packages are a strong default. Very large systems often combine them: top-level packages by feature, sub-packages by layer.
The Spring Modulith documentation (a framework you may meet later) describes a similar idea: it treats each direct sub-package of an application's main package as a module, and says that in a simple module, package-private types are hidden from other modules, while public types form its API. The same reasoning drives this lesson.
Honest trade-offs
Feature packages are not free:
- Deciding where a class belongs takes thought, whereas "it is a service" is automatic.
- Features may need each other, so you must watch dependencies.
- Generic code used everywhere (utilities, shared types) needs its own home. Do not let a package called
commonorutilbecome a dumping ground.
Justifying the choice
When the lab asks for two sentences, say what it gives you and why it matters. For example: "Feature packages keep everything for one business capability together, so a change or a removal touches one folder. They also let helpers be package-private, which keeps the public surface of each feature small."
Examples
Example 1: grouping a list of classes (simple)
Classes: InvoiceService, InvoiceRepository, PaymentService, PaymentGateway, InvoiceController.
By layer you would create controller/, service/ and repository/ plus a place for PaymentGateway. By feature:
billing/
invoice/ InvoiceController InvoiceService InvoiceRepository
payment/ PaymentService PaymentGateway(Whether invoice and payment are two packages or one billing package is a judgment call. If they change together, one package is fine. If you cannot decide, start with one.)
Example 2: layers force public (practical)
Package-by-layer code with the repository hidden does not compile. OrderRepository in shop.repository:
package shop.repository;
class OrderRepository {
String find(int id) { return "order-" + id; }
}OrderService in shop.service:
package shop.service;
import shop.repository.OrderRepository;
public class OrderService {
private final OrderRepository repo = new OrderRepository();
public String load(int id) { return repo.find(id); }
}The compiler rejects the import and both uses, with "OrderRepository is not public in shop.repository". To make it work you must write public class OrderRepository and public String find(...). After that, a Main in shop.app can write new OrderRepository().find(7) and gets order-7, skipping the service entirely.
Example 3: the feature version keeps the secret (realistic)
Both classes move into shop.order:
package shop.order;
class OrderRepository {
String find(int id) { return "order-" + id; }
}package shop.order;
public class OrderService {
private final OrderRepository repo = new OrderRepository();
public String load(int id) { return repo.find(id); }
}Main in shop.app imports only shop.order.OrderService:
package shop.app;
import shop.order.OrderService;
public class Main {
public static void main(String[] args) {
System.out.println(new OrderService().load(7));
}
}order-7The program works with less public than the layered version, and an attempt by shop.app to import OrderRepository fails with "is not public in shop.order". The business rules in the service cannot be bypassed.
Backend Insight
Most backend teams eventually face the "where does this class go?" question daily. Feature packages scale well with team structure: one team can own the order package, change its internals freely, and agree only on its small public surface with other teams. When a feature grows large enough, it can be pulled out into its own deployable service with little rewriting, because its dependencies are already visible and few. This is why the course's later material on modular monoliths starts from exactly this skill.
A warning from practice: a change that touches one feature should mostly touch one package. If most pull requests edit three or four packages at once, your grouping probably does not match how the system really changes.
Common Mistakes
Mistake 1: choosing layers because a tutorial did
Tutorials often show layered folders since they are easy to explain. That does not make them the best fit for your project. Ask what you will change together.
Mistake 2: feature packages that make everything public anyway
Moving files into feature folders does not hide anything by itself. You must also remove public from helpers. A feature package where every class is public has the same bloated surface as before.
Mistake 3: circular dependencies
If account imports customer and customer imports account, neither can be understood or moved alone. Let one depend on the other, or move the shared piece elsewhere.
Mistake 4: a giant common or util package
Dumping everything shared into one package recreates the "one giant package" problem. Place a class with the feature it belongs to.
Mistake 5: splitting too finely
One package per class defeats the purpose, since package-private hiding needs classes that share a package. Start with a few packages and split only when a package clearly has two unrelated responsibilities.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. In one sentence each, define package by layer and package by feature.
Show solution A1
A1. By layer: classes are grouped by technical role (controllers together, services together). By feature: classes are grouped by the business capability they serve (everything for orders together).
A2. Is package shop.service; a layer name or a feature name? What about package shop.invoice;?
Show solution A2
A2. shop.service is a layer name. shop.invoice is a feature name.
A3. Which grouping makes it easier to delete the whole "notification" capability?
Show solution A3
A3. Package by feature: delete the notification folder (after removing the imports that use it). By layer, notification classes are spread over several folders.
Level B: Application
B1. Group these into feature packages and give the package lines: ProductController, ProductService, ProductRepository, CartService, CartItem, CartController.
Show solution B1
B1. package shop.product; for ProductController, ProductService, ProductRepository; package shop.cart; for CartService, CartItem, CartController.
B2. In your grouping from B1, which classes would you keep package-private, and which public? Assume only the controllers and services are called from outside their feature.
Show solution B2
B2. Public: ProductController, ProductService, CartController, CartService. Package-private: ProductRepository and CartItem. (If another feature needs CartItem as a type it receives, it would become public: decide by who needs it.)
B3. In a layered layout, CartService in shop.service needs CartItem from shop.model. What must be true about CartItem and why?
Show solution B3
B3. CartItem must be public (and any members CartService calls must be public), because the two classes are in different packages. That also opens CartItem to every other package.
Level C: Challenge
C1. The cart package imports shop.product.Product. A teammate proposes that product also imports shop.cart.Cart to count how many carts hold a product. What problem would this create, and what alternative could you suggest?
Show solution C1
C1. It would create a circular dependency between cart and product, so neither can be understood or moved without the other. Alternative: ask the cart feature for the count (cart depends on product, one direction) or compute it in a place that depends on both.
C2. Write two sentences justifying feature packaging for a small online shop, as you would in a lab report. Then give one drawback you would accept.
Show solution C2
C2. For example: "Feature packages keep everything for one capability together, so a change or removal touches one folder. They let helpers be package-private, which keeps each feature's public surface small." A drawback to accept: deciding where shared classes belong needs thought, and features must be watched for circular dependencies.
Mini Challenge
Draw both folder trees for a small library application with these classes: BookController, BookService, BookRepository, MemberController, MemberService, MemberRepository, LoanService, LoanRepository.
Check your work: the layered tree has three package folders; the feature tree has three feature folders (book, member, loan); and for the feature tree you wrote which classes would be public and which package-private, with the public surface per feature no more than the controller and service (what other features really need). Also mark one dependency between features (for example loan uses book) and check it points one way only.
Summary & Next Lesson
Key takeaways
- By layer groups classes by technical role. By feature groups them by business capability.
- Layer packages force cross-layer classes to be
public, which enlarges the public surface and lets callers skip rules. - Feature packages let helpers be package-private and keep a feature's code in one place, easy to find and remove.
- Imports between feature packages show dependencies. Keep them few and one-directional.
- Both styles are used in industry. For small and medium applications, feature packages are a strong default. Layering can still exist inside a feature.
- Watch the trade-offs: unclear placement of shared code, and circular dependencies.
Next lesson
Time to apply everything. The next lesson, "Demo: reorganizing Bank Accounts into feature packages", takes a project with all classes in the default package and splits it into account, customer and notification packages with a minimal public surface.
Self-check
- Name one layer package and one feature package.
- Why must a repository be public in a layered layout when the service lives in another package?
- Which grouping allows an
OrderRepositoryto be package-private? - What problem does a circular dependency between two packages cause?
- Is layering "wrong"? Explain briefly.
Show answers
- For example
service(layer) andorder(feature). - They are in different packages, and only public classes are visible across packages.
- Package by feature, with the repository in the same package as its service.
- Neither package can be understood, changed or moved without the other.
- No. Layers are a useful way to think about responsibilities. The point is how classes are grouped into packages, and layers can exist inside a feature.
References
- Spring, Spring Modulith fundamentals. Supports the statement that sub-packages can act as modules whose package-private types are hidden and whose public types form the API.
- Oracle, The Java Tutorials: Controlling Access to Members of a Class. Supports the rule that package-level access is limited to the same package, which is why cross-package use needs
public. - The layer-versus-feature comparison, its trade-offs and the "screaming architecture" nickname are common community design practice that we did not verify against a single authoritative source. The compiler errors and outputs were produced with JDK 21.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.