Switching Grails data sources with Spring profiles
A Grails application rarely needs only one database for its whole life. A developer may use PostgreSQL on a laptop, an isolated test database in CI, and a managed cluster in production. Editing one JDBC URL for every situation invites accidental writes to the wrong place and makes deployment depend on manual steps. Spring Profiles provide a clearer boundary: select a named environment, and Grails receives the matching datasource settings as its application context starts.
This pattern suits Australian teams whose members may sit in Sydney, Melbourne, Brisbane or Perth and join an early video stand-up before the workday begins. It also fits businesses operating in the local market, where order values may be displayed in AUD and tax records must distinguish GST from other amounts. A shared codebase can support every environment without changing application logic, provided the boundary is explicit and the database is never selected by a value entered in a browser.
Profiles are configuration selectors, not substitutes for infrastructure management, secret storage or schema migration. They choose which datasource definition becomes active; they do not copy data, create databases or provide transaction isolation. Used carefully in Grails, they make the intended environment visible, keep environment-specific values out of core code, and let one packaged application run across a workstation, test pipeline and Australian cloud region.
Why Spring Profiles fit Grails
Modern Grails releases use Spring Boot, so profile handling comes from Spring rather than a Grails-only convention. A base configuration can hold shared settings, while application-development.yml, application-test.yml and application-production.yml contribute values when the matching profile is active. This keeps application-wide settings such as JPA behaviour in one place and limits endpoints, credentials and environment policy to focused files.
Profiles are resolved while the application context is being created. Selecting production gives GORM, connection pooling, migrations and any bean that reads the datasource one stable target for that process. A request cannot casually switch the whole application to another database, which is a major safety benefit. If traffic needs a reporting replica or a tenant-specific database, plan a routing datasource or separate services explicitly instead of changing spring.datasource.url at runtime.
Profile names also give developers, CI jobs and deployment platforms a common vocabulary. Treat an unknown or missing profile as a startup failure rather than allowing an accidental fallback to production. This turns “which database?” from tribal knowledge into a property of the running process that the team can inspect and enforce.
Separate shared and profile-specific settings
Keep shared settings in grails-app/conf/application.yml and place profile overrides in sibling files. An application using versioned migrations can begin with:
spring:
application:
name: orders
jpa:
open-in-view: false
hibernate:
ddl-auto: validate
flyway:
enabled: true
The Flyway properties apply when Flyway is on the classpath. For a local PostgreSQL database, create application-development.yml:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/orders_dev
username: ${DB_USERNAME:orders_dev}
password: ${DB_PASSWORD:local-only}
driver-class-name: org.postgresql.Driver
Local defaults keep onboarding simple, while environment variables allow a shared workstation to use a managed development account. Production should contain no embedded secret fallback:
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: org.postgresql.Driver
A missing production placeholder then fails startup instead of selecting a convenient but unsafe value. Grails 2 applications need a different mechanism, such as environment-specific Config.groovy scripts, because Spring profile files are not equivalent there.
Activating profiles across environments
Activate a profile through the environment, an IDE run configuration or a command-line property. Environment variables are useful because the same command works on macOS, Linux and CI workers:
SPRING_PROFILES_ACTIVE=development ./grailsw run
SPRING_PROFILES_ACTIVE=test ./grailsw test
SPRING_PROFILES_ACTIVE=production java -jar build/libs/orders.jar
For the active-profile setting, a command-line argument generally takes precedence over the environment variable, which takes precedence over a value packaged in application.yml. Check the deployment platform’s configuration syntax carefully because an injected but malformed environment variable can be ignored or quoted as part of another value.
Avoid making production the packaged default, and think carefully before placing development in the base file. A local convenience can become a production surprise when an operator forgets to override it. Declare the intended profile in deployment manifests, require it explicitly for hosted releases, and make startup report the selected profiles and a safe database identifier.
Protecting credentials and Australian data
Profile files are suitable for source control when they contain placeholders, local examples or non-secret endpoints. Keep populated .env files and secret-manager exports out of Git. In production, inject credentials at runtime, restrict the database account to the schemas and operations the application needs, require TLS, and separate migration permissions from the permissions used by normal requests.
For organisations covered by the Australian Privacy Act 1988, the Australian Privacy Principles require reasonable security and access controls; APP 11 and APP 12 are particularly relevant to system security and access/correction requests. Development and test profiles should normally use synthetic or irreversibly anonymised records rather than a copy of production customer data. The Privacy Act does not generally prescribe Australian-only storage, so choosing a Sydney or Melbourne cloud region does not replace privacy, contractual or sector-specific controls.
A database located in a Sydney or Melbourne cloud region can reduce latency for nearby users, but residency alone is not resilience. Define backup retention, point-in-time recovery and regional failover for the selected service. Keep separate credentials and separate databases for each environment even when they run in the same region, so a damaged development process cannot directly alter production records.
Testing databases and running migrations
The test profile should target a disposable database owned by the test run, not a developer’s working schema or a shared QA database. A CI service container or Testcontainers PostgreSQL instance gives the suite the same SQL dialect as production. An in-memory database can speed up small unit tests, but a production-engine test should still exercise constraint handling, locking and transaction behaviour.
Run migrations before integration specifications and keep ddl-auto at validate for deployed environments. Apply the same reviewed migration chain with Flyway, Liquibase or the Grails migration plugin, and record its version in the deployment runbook. For background on the surrounding persistence and deployment workflow, the site's Grails magazine archive offers a useful reference for related Grails lessons.
Changing the profile changes the connection target; it does not make incompatible schemas equivalent. Tests should cover migrations from the previous release, failed migration recovery, empty-result cases and database-specific behaviour such as case-sensitive identifiers. Never let a test profile point at production even temporarily, since an incorrect environment variable can turn a test suite into a data incident.
Deploying one artifact to several environments
Build one immutable Grails artefact and promote it through test, staging and production. Each stage should receive the same artefact plus configuration appropriate to its Spring profile. Container manifests, ECS tasks or Kubernetes deployments can set SPRING_PROFILES_ACTIVE and the datasource variables from a secret store, while CI checks that required production properties are present before release.
Consider a sports-results dashboard modelled on the kind of content shown by esports results coverage. Local fixtures, a sanitised test feed and the production feed would each need a different datasource and access policy. The public site provides the content context, while the application team still owns the private connection settings and migration process.
A profile switch does not migrate data. If a release moves from one database to another, copy and verify the data, run compatibility migrations and switch the application only after both sides are ready. For major schema changes, use expand-and-contract releases so old and new application versions can operate during a rolling deployment. Back up the source database and define rollback behaviour before changing the active URL.
Diagnosing profile and datasource failures
When an application reaches the wrong database, diagnose profile activation before investigating the JDBC driver. Inspect the startup log, the effective configuration endpoint if the application exposes one, and the values injected by the host. Confirm that profile-specific files sit under grails-app/conf, are included in the artefact and use filenames Spring recognises. Restrict diagnostic endpoints because environment endpoints may reveal hostnames or variable names.
Common failures include absent environment variables, a missing profile file, an overridden property, an incorrect driver class and a URL whose special characters were not encoded. If a password contains @, : or /, prefer a JDBC URL builder or encode the value correctly. Comparing the configured URL with the host’s injected environment also reveals subtle errors such as an extra space or quote character.
Schema validation failures often mean the profile is correct but migrations were skipped. Run the expected migration job, then retry. Remember that connection pools and Hibernate metadata are initialised at startup, so changing an environment variable does not reconfigure a live process; restart the application after changing its target. Log the profile, driver, host and database name for diagnosis, but never log the password or a URL containing embedded credentials.
Put this pattern into practice now with one Grails service. Create development, test and production profile files, move credentials to environment-provided secrets, and run the same build against a disposable test database. Record each profile’s intended database and required migration in the team’s deployment runbook.
Make environment selection an explicit deployment input, fail fast when production values are missing, and verify the connected database before accepting traffic. Proving the pattern on a small service creates a reliable template for further Grails applications without scattering hidden configuration across developer machines and hosting platforms.