Add an internal-only field
Goal: add a column to an existing entity that the city uses internally (a query, a report, a scheduled job) but never accepts or returns on the public API. This is the lightest kind of data-model extension — no DTO, use case or controller changes.
- Seam: the
*Base@MappedSuperclass+ the@NoRepositoryBeangeneric repository. - City files: a subclass entity, a bound repository, an additive migration.
- Schema change: additive
V2+Flyway migration. - Applies to: both topologies.
Why it works
Each aggregate splits its columns into a <Entity>Base (@MappedSuperclass, all platform
columns) plus a thin concrete @Entity, and the repository is generic
(CityPlaceRepository<T extends CityPlaceBase>, marked @NoRepositoryBean). A city adds its
own @Entity extending the base with the extra column and binds the generic repository to
it — the platform-owned columns are never forked.
Recipe
1. The migration
Cities add additive V2+ migrations in their deployable's
src/main/resources/db/migration, so adding a column never conflicts with a platform
upgrade.
-- V2__add_internal_priority_to_city_places.sql
ALTER TABLE city_places
ADD COLUMN internal_priority INT NOT NULL DEFAULT 0;
2. The city entity + repository
// com.modelcity.leisure.cityplaces.repository.model.CityPlaceWithPriority
@Entity
@Table(name = "city_places")
@SuperBuilder
@NoArgsConstructor
@Getter
@Setter
public class CityPlaceWithPriority extends CityPlaceBase {
@Column(name = "internal_priority", nullable = false)
private int internalPriority;
}
// com.modelcity.leisure.cityplaces.repository.CityPlaceWithPriorityRepository
public interface CityPlaceWithPriorityRepository extends CityPlaceRepository<CityPlaceWithPriority> {
List<CityPlaceWithPriority> findByInternalPriorityGreaterThanOrderByInternalPriorityDesc(int min);
}
3. Use it from your own internal code
@Service
@RequiredArgsConstructor
public class CityPriorityReport {
private final CityPlaceWithPriorityRepository repository;
public List<CityPlaceWithPriority> topPriorityPlaces() {
return repository.findByInternalPriorityGreaterThanOrderByInternalPriorityDesc(0);
}
}
That is all: no CityPlaceDto, no use case, no controller change. The public
GET /city-places keeps returning the platform shape.
Notes
- The platform
CityPlaceentity stays on the classpath and remains mapped tocity_places. Two entities mapping one table is legal and intended here — the platform default store keeps usingCityPlace, your internal code usesCityPlaceWithPriority. Both see the same rows; only your entity sees the new column. - If the platform write path must populate the column, this becomes a Replace a persistence adapter job (bind the store to your entity). If the column must also appear on the API, see Expose a new field on the API.
- Keep the column nullable or defaulted so the platform's own inserts (which don't know about it) still succeed.
Verify
mvn -q -pl <your-app> flyway:migrate # or start the app with FLYWAY_ENABLED=true
jpa.hibernate.ddl-auto=validate (the archetype default) makes the app fail fast on
startup if the entity and schema disagree — a green start proves the mapping and the
migration line up. Then hit your internal report and confirm GET /city-places is
unchanged.