Cross-Cutting Concerns & Putting It Together

The pieces that touch every request — and one request followed through all of them
Big idea: Some concerns do not belong to any single endpoint: knowing who is calling and what data they may see, recording what happened, stamping rows with when, and behaving differently per environment. This chapter collects those, then traces a single HTTP call from socket to SQL and back so every previous chapter clicks into place.

What You Will Learn

  • Request-scoped context: CurrentUserService and branch scoping.
  • Activity logging as a cross-cutting concern, and where AOP would fit.
  • Auditing columns (created_at / updated_at) and @CreationTimestamp vs DB defaults.
  • Configuration & profiles; @Value vs @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 context7.2 Activity logging7.3 Auditing columns7.4 Configuration & profiles7.5 Package structure7.6 End-to-end trace7.7 Testing map7.8 Wrap-up

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 context JwtAuthFilter populated (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: ReportsService and BillService call resolveEffectiveBranchId(branchId) at the top of every query, so a Doctor physically cannot read another branch's numbers even by passing ?branchId=.
Pattern: when several services need "the current user" or a derived scope, wrap the 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.

ApproachTrade-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 methodsZero clutter in services, uniform coverage; harder to give each event a human-readable message, and indirection can surprise.
Domain eventsApplicationEventPublisher + @EventListenerDecoupled, 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.

StrategyWho writes the timestamp
DB default columns (this project)MySQL, on insert/update
Hibernate @CreationTimestamp / @UpdateTimestampHibernate, 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=...
This repo checks a real DB password and JWT secret into 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-featurePackage-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:

  1. CORS — browser pre-flights OPTIONS /api/visits; SecurityConfig permits OPTIONS and the CORS source returns the allow headers. (Ch. 6)
  2. Filter chain — real POST arrives. JwtAuthFilter reads Authorization: Bearer …, validates the signature and expiry via JwtUtil, checks revoked_token by jti, loads the User by email, sets an Authentication with ROLE_Doctor in the SecurityContextHolder. (Ch. 6)
  3. Authorization anyRequest().authenticated() passes; no @PreAuthorize on createVisit, so any authenticated user may proceed. (Ch. 6)
  4. Dispatch DispatcherServlet matches VisitController.createVisit. Jackson deserialises the JSON into VisitRequest; @Valid runs its constraints (400 on failure). (Ch. 2, Ch. 5)
  5. Service @Transactional createVisit opens a transaction. Guard clauses resolve patient / doctor / appointment or throw ResponseStatusException (400 / 409, which also rolls back). Branch is resolved via CurrentUserService. (Ch. 4, Ch. 7)
  6. Repositories — Spring Data proxies issue INSERTs for visit, prescription, prescription_item(s), lab_test(s) and an UPDATE to link the bill; all in the persistence context. (Ch. 3)
  7. Cross-cutting activityLogService.log("completed consultation for", name) inserts an audit row; created_at is stamped by the DB. (Ch. 7)
  8. Commit — method returns normally → transaction commits; every row lands atomically. (Ch. 4)
  9. Response — service returns VisitResponse (a DTO, mapped while entities were still managed); controller wraps it ResponseEntity.ok(...); Jackson writes JSON; filters unwind; SecurityContext is cleared. (Ch. 2)
CORSJwtAuthFilterauthorizecontroller + @Valid@Transactional servicerepositoriesactivity logcommitDTO → JSON

7.7 Where Tests Attach

LayerToolWhat 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 mockedRouting, status codes, @Valid 400s, JSON shape.
Security@WebMvcTest + spring-security-test@PreAuthorize allow/deny, 401 vs 403.
Full app@SpringBootTest + TestRestTemplateLogin → 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_atinsertable=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

  1. Reading the current user from a controller parameter instead of the security context, then passing it down everywhere.
  2. Trusting branchId from the client for non-admins (the whole point of resolveEffectiveBranchId).
  3. Setting created_at in Java while the column also has a DB default — two sources of truth.
  4. Committing real secrets; forgetting relaxed binding lets JWT_SECRET override jwt.secret.
  5. Expecting application-prod.yaml to load without SPRING_PROFILES_ACTIVE=prod.
  6. Scattering SecurityContextHolder calls instead of one bean.
  7. Only a contextLoads() test and calling it covered.
  8. Putting entities in feature packages, then fighting circular package dependencies when a service needs several.

Revision Questions

  1. How does CurrentUserService know who is calling without a parameter?
  2. Why is branch re-derived from the DB on every request rather than read from the JWT?
  3. Give the three ways to populate created_at and say which this project uses.
  4. What does insertable = false, updatable = false tell Hibernate?
  5. How do you override jwt.secret in production without editing files?
  6. Contrast package-by-feature and package-by-layer; why keep entity/ shared?
  7. List, in order, the stages a POST /api/visits passes through.
  8. At which stage would a 409 be produced, and what happens to the writes already made?
  9. Match each layer to the test tool you would use.

Practice

1. Externalise secrets. Replace the literal jwt.secret and datasource password with ${JWT_SECRET} / ${SPRING_DATASOURCE_PASSWORD} placeholders and run with env vars.
2. Profile. Add application-dev.yaml with show-sql: true and a prod file with it off; switch with SPRING_PROFILES_ACTIVE.
3. Test a rule. Write a Mockito test for resolveEffectiveBranchId: Admin keeps the requested id; Doctor is forced to their own branch regardless of input.
4. Web slice. With @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.

You have finished the book. Re-open 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.