Spring Boot has no opinion about your package layout, which means the decision is yours and it compounds. This lesson describes the structure the pizza API uses and the reasoning behind each choice — including the one mistake that produces a genuinely baffling error.
The one rule you cannot break
@SpringBootApplication includes @ComponentScan, which scans the
annotated class's package and everything below it. Nothing else.
com.pizza.api
├── PizzaSpringbootBackendApplication.java <- scanning starts here
├── config/ scanned
├── entity/ scanned
└── security/ scanned
com.pizza.util NOT scanned - a sibling, not a childPut a @Service in com.pizza.util and Spring never sees it. The failure is
not "package not scanned" — it is:
Parameter 0 of constructor in com.pizza.api.entity.order.CustomerOrderServiceImpl
required a bean of type 'com.pizza.util.TaxCalculator' that could not be found.Which sends you looking for a missing @Service annotation that is right there. Keep
the application class in the root package of your application and this problem never
occurs.
Package by feature, not by layer
The layout most tutorials show groups by technical role:
com.example.app
├── controller/ ProductController, OrderController, UserController, CartController...
├── service/ ProductService, OrderService, UserService, CartService...
├── repository/ ProductRepository, OrderRepository, UserRepository...
└── model/ Product, Order, User, Cart...It looks tidy and it works badly at scale. Every change to one feature touches four directories,
and no directory tells you what the application does. Open controller/ in a mature
project and you are looking at forty files with nothing in common.
The pizza API groups by domain instead:
com.pizza.api
├── config/ SecurityConfig · OpenApiConfig · RestMVCConfig · ThreadPoolConfig
│ CacheConfig · PizzaProperties
├── aspect/ ServiceTimingAspect
├── dto/ every DTO + ONE central EntityDTOMapper (MapStruct)
├── entity/ DatabaseTableNames + one package per domain:
│ ├── cart/ Cart, CartItem, CartItemTopping + DAO/Service/Controller
│ ├── crust/
│ ├── order/ CustomerOrder, OrderItem, PricingService, OrderPlacedEvent
│ ├── product/ Product, ProductSize
│ ├── topping/
│ └── user/ User, UserAddress, UserPaymentMethod, auth + profile + admin
├── exception/ ApiError · ApiSubError · ApiException · RestExceptionHandler
├── mail/ MailService + Impl
├── mapper/ JdbcTemplate RowMapper classes
├── messaging/ JMS publisher, listener and config
├── payment/ StripeService · StripeWebhookController
├── report/ ReportDAO/Imp · ReportService/Impl · ReportRestController
├── search/ Elasticsearch document, repository, service
├── security/ JwtService · JwtAuthenticationFilter · oauth2/
└── storage/ ProductImageStorageService + ImplEverything about products lives in entity/product/: the entity, its DAO, its service,
its controller. Deleting the feature means deleting a directory. Understanding it means reading
one.
Interface plus implementation, always
Every service and every DAO in the pizza API is an interface and a separate class:
entity/product/
├── Product.java the JPA entity
├── ProductRepository.java Spring Data - derived queries
├── ProductDAO.java the data-access interface
├── ProductDAOImp.java repository + JdbcTemplate
├── ProductService.java the business interface
├── ProductServiceImpl.java the implementation
├── ProductRestController.java HTTP
├── ProductType.java enums
└── SizeName.javapublic interface ProductService {
List<ProductDTO> getMenu();
List<ProductDTO> getByType(ProductType type);
ProductDTO getProductByPublicId(UUID id);
ProductDTO createProduct(ProductCreateDTO dto);
ProductDTO updateProduct(UUID id, ProductCreateDTO dto);
ProductDTO setProductImage(UUID id, MultipartFile file);
void deactivateProduct(UUID id);
void deleteProduct(UUID id);
}Is the interface worth it when there is exactly one implementation? Reasonable people disagree. The argument for it here: the interface is the readable summary of what the feature does — eight lines against three hundred — and it is what the controller and the tests depend on. The argument against is that it is ceremony. Pick one and be consistent; the cost of mixing both styles is higher than the cost of either.
The DAO layer
This is the pizza API's most opinionated choice. Each DAO implementation wires in both a
Spring Data repository and a JdbcTemplate, and uses whichever fits the method:
- Repository for simple things — save, single-row lookups, existence checks. It
returns managed entities that dirty checking can track, and it honours the
@SQLRestrictionthat hides soft-deleted rows. JdbcTemplatefor anything that aggregates or whose result is not an entity. JPA has nothing to offer those.
Lessons 16 and 17 cover each in depth. The structural point is that the choice is made inside the DAO, so the service above it never knows or cares which was used.
Where the cross-cutting pieces go
| Package | Holds | Why not in the feature package |
|---|---|---|
config/ | Security, OpenAPI, thread pools, caching, typed properties | They configure the application, not one domain |
exception/ | The error shape and one
@RestControllerAdvice | One error contract for every endpoint — lesson 12 |
dto/ | Every DTO and one central MapStruct mapper | DTOs cross feature boundaries; one mapper avoids twelve |
security/ | JWT service and filter | Applies to every request |
A reasonable alternative is to keep each feature's DTOs in its own package. The pizza API chose
one dto/ package because a single EntityDTOMapper then has one obvious
home; with per-feature DTOs you get per-feature mappers, which is more files and more places for the
mapping conventions to drift.
Naming
Consistency matters more than the specific convention, but two are worth calling out:
CustomerOrder, notOrder.ORDERis a SQL reserved word, and an entity calledOrdergenerates SQL that needs quoting in every query. Renaming the class is cheaper than fighting it.- Pick
ImplorImpand never mix them. The pizza API usesDAOImpandServiceImpl— inherited from the codebase it was patterned on, and inconsistent. It is consistent within each layer, which is enough to be predictable, and changing it now would touch every file for no functional gain.
What to take from this
- Application class in the root package. Everything else is preference; this one produces a misleading error.
- Group by feature. A directory should tell you what the app does, not what layer it is.
- Be consistent about interfaces and naming. Half-applied conventions are worse than either extreme.
- Cross-cutting code goes in its own package, feature code stays with its feature.
Next: beans, scopes and lifecycle — what the container
is actually holding, and why @Primary and @Qualifier exist.