Cross-Cutting Concerns & Putting It Together
What You Will Learn
- Request-scoped context:
CurrentUserServiceand branch scoping. - Activity logging as a cross-cutting concern, and where AOP would fit.
- Auditing columns (
created_at/updated_at) and@CreationTimestampvs DB defaults. - Configuration & profiles;
@Valuevs@ConfigurationProperties; secret management. - Package-by-feature vs package-by-layer — this project does both.
- The full request lifecycle, end to end.
- Where tests attach at each layer.
Chapter Structure
7.1 Request-Scoped Context: CurrentUserService
@Service
@RequiredArgsConstructor
public class CurrentUserService {
private final UserRepository userRepository;
public Optional<User> getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) return Optional.empty();
return userRepository.findByEmail(authentication.getName());
}
/**
* Admin: honours an explicit branchId (or null = all branches).
* Doctor / Receptionist: always forced to their own assigned branch —
* any client-supplied branchId is ignored entirely.
*/
public Long resolveEffectiveBranchId(Long requestedBranchId) {
User user = getCurrentUser().orElse(null);
if (user == null || "Admin".equalsIgnoreCase(user.getRole().getName())) {
return requestedBranchId;
}
return user.getBranch() != null ? user.getBranch().getId() : null;
}
}- It reads identity from the
SecurityContextHolder— the same contextJwtAuthFilterpopulated (Chapter 6) — so it needs no method parameter to know the caller. - Branch is always re-derived from the DB, never taken from the token. An Admin reassigning a user's branch takes effect on that user's very next request.
- It centralises an authorization data rule:
ReportsServiceandBillServicecallresolveEffectiveBranchId(branchId)at the top of every query, so a Doctor physically cannot read another branch's numbers even by passing?branchId=.
SecurityContextHolder access in one injectable bean rather than repeating it.7.2 Activity Logging
@Service
@RequiredArgsConstructor
public class ActivityLogService {
private final ActivityLogRepository activityLogRepository;
private final UserRepository userRepository;
public void log(String action, String target) {
ActivityLog entry = new ActivityLog();
entry.setActorName(resolveActorName()); // from SecurityContext, or "System"
entry.setAction(action);
entry.setTarget(target);
activityLogRepository.save(entry);
}
}Services call it explicitly at meaningful moments — activityLogService.log("registered new patient", name) in PatientService, "completed consultation for" in VisitService. Like CurrentUserService, it discovers the actor from the security context, defaulting to "System" for unauthenticated / scheduled callers.
| Approach | Trade-off |
|---|---|
| Explicit calls (this project) | Full control of wording and which events matter; costs a line per event and is easy to forget. |
AOP aspect — @Around on annotated methods | Zero clutter in services, uniform coverage; harder to give each event a human-readable message, and indirection can surprise. |
Domain events — ApplicationEventPublisher + @EventListener | Decoupled, testable; more moving parts than a small app needs. |
7.3 Auditing Columns
Entities like Patient, User, Appointment carry timestamps mapped read-only:
@Column(name = "created_at", nullable = false, insertable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false, insertable = false, updatable = false)
private LocalDateTime updatedAt;insertable = false, updatable = false means Hibernate reads these columns but never writes them — the database fills them via DEFAULT CURRENT_TIMESTAMP / ON UPDATE CURRENT_TIMESTAMP. That is why PatientResponse can echo them but no service ever sets them.
| Strategy | Who writes the timestamp |
|---|---|
| DB default columns (this project) | MySQL, on insert/update |
Hibernate @CreationTimestamp / @UpdateTimestamp | Hibernate, in Java |
Spring Data JPA auditing (@CreatedDate, @EnableJpaAuditing) | Spring, and can also record @CreatedBy |
7.4 Configuration & Profiles
Everything environment-specific is in application.yaml: datasource URL / credentials, server.port, jwt.secret, jwt.expiration, jpa.hibernate.ddl-auto, devtools settings.
# Read one value
@Value("${jwt.secret}") private String secret;
# Activate an environment
SPRING_PROFILES_ACTIVE=prod java -jar app.jar
# → application.yaml is the base, application-prod.yaml overrides it
# Override a single key without touching files (highest practical priority)
export JWT_SECRET=... # relaxed binding: JWT_SECRET → jwt.secret
export SPRING_DATASOURCE_PASSWORD=...application.yaml. In production, keep the file with placeholders (or profile-specific files out of VCS) and supply the real values as environment variables / a secrets manager. The relaxed-binding rule (JWT_SECRET → jwt.secret) makes this a drop-in change.@Value | @ConfigurationProperties(prefix = "jwt") |
|---|---|
| One key → one field. | A whole prefix → a typed bean, with validation & IDE hints. |
What this project uses (only two jwt.* keys). | Worth it once a prefix has many keys. |
7.5 Package Structure
The codebase mixes two organising styles:
com.example.hospital_erp
├── HospitalErpApplication.java
│
│ # package-by-feature (a vertical slice per use case)
├── auth/ { controller, service, jwt, dto }
├── patient/ { controller, service, dto }
├── appointment/ { controller, service, dto }
├── visit/ bill/ receipt/ doctor/ medicine/ reports/ dashboard/ ...
│
│ # package-by-layer (shared, cross-feature)
├── entity/ { Patient, User, Appointment, Bill, ... } ← all JPA entities together
├── repository/ { PatientRepository, ...Projection interfaces } ← all repositories together
├── config/ { SecurityConfig }
└── security/ { CurrentUserService }| Package-by-feature | Package-by-layer |
|---|---|
| Everything for "billing" in one folder; easy to navigate a use case; deletes cleanly. | All entities / repositories in one place; fine when they are shared by many features (as they are here). |
The pragmatic hybrid: slice controllers/services/DTOs by feature, keep the shared persistence model (entity/, repository/) together because VisitService alone touches eight repositories.
7.6 End-to-End: One Request
POST /api/visits with a bearer token and a VisitRequest body. Follow it through every chapter:
- CORS — browser pre-flights
OPTIONS /api/visits;SecurityConfigpermitsOPTIONSand the CORS source returns the allow headers. (Ch. 6) - Filter chain — real
POSTarrives.JwtAuthFilterreadsAuthorization: Bearer …, validates the signature and expiry viaJwtUtil, checksrevoked_tokenbyjti, loads theUserby email, sets anAuthenticationwithROLE_Doctorin theSecurityContextHolder. (Ch. 6) - Authorization —
anyRequest().authenticated()passes; no@PreAuthorizeoncreateVisit, so any authenticated user may proceed. (Ch. 6) - Dispatch —
DispatcherServletmatchesVisitController.createVisit. Jackson deserialises the JSON intoVisitRequest;@Validruns its constraints (400 on failure). (Ch. 2, Ch. 5) - Service —
@Transactional createVisitopens a transaction. Guard clauses resolve patient / doctor / appointment or throwResponseStatusException(400 / 409, which also rolls back). Branch is resolved viaCurrentUserService. (Ch. 4, Ch. 7) - Repositories — Spring Data proxies issue INSERTs for
visit,prescription,prescription_item(s),lab_test(s) and an UPDATE to link thebill; all in the persistence context. (Ch. 3) - Cross-cutting —
activityLogService.log("completed consultation for", name)inserts an audit row;created_atis stamped by the DB. (Ch. 7) - Commit — method returns normally → transaction commits; every row lands atomically. (Ch. 4)
- Response — service returns
VisitResponse(a DTO, mapped while entities were still managed); controller wraps itResponseEntity.ok(...); Jackson writes JSON; filters unwind;SecurityContextis cleared. (Ch. 2)
7.7 Where Tests Attach
| Layer | Tool | What you assert |
|---|---|---|
| Service (pure) | JUnit + Mockito, new Service(mocks) | Rules, mapping, guard-clause statuses. No Spring. |
| Repository | @DataJpaTest (+ Testcontainers MySQL) | Derived queries, @Query JPQL, projections. |
| Controller (web slice) | @WebMvcTest + MockMvc, service mocked | Routing, status codes, @Valid 400s, JSON shape. |
| Security | @WebMvcTest + spring-security-test | @PreAuthorize allow/deny, 401 vs 403. |
| Full app | @SpringBootTest + TestRestTemplate | Login → call → assert, through the real chain. |
The project currently ships the generated HospitalErpApplicationTests.contextLoads() only — a smoke test that the ApplicationContext starts. The test starters are already in pom.xml.
Quick Concept Map
CurrentUserService → one place to read the caller + derive branch scope from the DB
ActivityLogService → explicit audit events; AOP / domain events are the alternatives
created_at / updated_at → insertable=false, updatable=false; DB writes them
application.yaml + profiles + env vars → config precedence; keep secrets out of VCS
@Value for a key or two · @ConfigurationProperties for a prefix
feature packages for slices · layer packages for the shared model
Request lifecycle: CORS → filter → authorize → controller/@Valid → @Transactional service → repositories → commit → DTO→JSON
Common Mistakes
- Reading the current user from a controller parameter instead of the security context, then passing it down everywhere.
- Trusting
branchIdfrom the client for non-admins (the whole point ofresolveEffectiveBranchId). - Setting
created_atin Java while the column also has a DB default — two sources of truth. - Committing real secrets; forgetting relaxed binding lets
JWT_SECREToverridejwt.secret. - Expecting
application-prod.yamlto load withoutSPRING_PROFILES_ACTIVE=prod. - Scattering
SecurityContextHoldercalls instead of one bean. - Only a
contextLoads()test and calling it covered. - Putting entities in feature packages, then fighting circular package dependencies when a service needs several.
Revision Questions
- How does
CurrentUserServiceknow who is calling without a parameter? - Why is branch re-derived from the DB on every request rather than read from the JWT?
- Give the three ways to populate
created_atand say which this project uses. - What does
insertable = false, updatable = falsetell Hibernate? - How do you override
jwt.secretin production without editing files? - Contrast package-by-feature and package-by-layer; why keep
entity/shared? - List, in order, the stages a
POST /api/visitspasses through. - At which stage would a 409 be produced, and what happens to the writes already made?
- Match each layer to the test tool you would use.
Practice
jwt.secret and datasource password with ${JWT_SECRET} / ${SPRING_DATASOURCE_PASSWORD} placeholders and run with env vars.application-dev.yaml with show-sql: true and a prod file with it off; switch with SPRING_PROFILES_ACTIVE.resolveEffectiveBranchId: Admin keeps the requested id; Doctor is forced to their own branch regardless of input.@WebMvcTest + MockMvc, assert POST /api/doctors returns 403 for a Receptionist and 201 for an Admin.Chapter Wrap-Up
Cross-cutting concerns are the seams of a Spring Boot app: CurrentUserService turns the security context into a reusable "who and what scope", ActivityLogService records intent, database-managed timestamps record time, and application.yaml plus profiles and environment variables adapt one build to many environments. Trace a single request and every earlier chapter appears in order — IoC wired the beans, the filter chain proved identity, the controller bound input, the service enforced rules in a transaction, repositories persisted rows, and a DTO went back as JSON.
VisitService.createVisit now — it should read as a summary of everything here: injection, DTOs, repositories, @Transactional, guard clauses, the security context, and activity logging, all in one method.Teaching note: snippets are quoted from security/CurrentUserService.java, activity/service/ActivityLogService.java, the entity/ package and src/main/resources/application.yaml.