Skip to content

Latest commit

 

History

History
93 lines (70 loc) · 6.93 KB

scala.md

File metadata and controls

93 lines (70 loc) · 6.93 KB

Scala

  • Always specify an explicit sbt.version
  • Ensure you run in production with GC logging enabled
  • Prefer immutability
  • Prefer the latest version of Scala. At the time of writing 3.1.2 is the latest version, so 2.13.8 may be acceptable. if you are still using 2.12.x you should plan to migrate.
  • If some dependencies are unavailable for the latest version, it is acceptable to use the latest patch version of the previous minor version but try to avoid it. If you have a dependency that isn't available for one of these versions, it's effectively dead and you should plan how to move away from it.
  • If you come to a codebase using an older version of Scala, it should be upgraded before any new features are added.
  • Run on a Corretto LTS version of Java, no lower than Java 21. For tips on upgrading from Java 8 or Java 11, see here.
  • Prefer passing dependencies as function parameters over dependency injection1. This enable referential transparency of your programs.

Continuous dependency management

Use Scala Steward to manage the dependencies of your Scala codebases. It's important to keep up with changes to dependencies so that when a large-scale security problem occurs there's no need to do a massive update of multiple dependencies all at once. Scala Steward works in a similar way to Dependabot, by monitoring a codebase on a schedule and raising a PR whenever a new version of a dependency becomes available. Here's an example of a generated PR, showing the dependency changelog and diff between the version currently in use and the new one.

Scala Steward also makes use of Scalafix code fixups, where they're available, to reduce the workload a little. This can be of variable benefit. The fix will be applied as a separate commit. (Example)

Once Scala Steward has been integrated into a repo, it's best to get into the habit of testing and either merging or closing all the outstanding PRs it has generated before doing any work on the codebase.

Bear in mind that the first run of Scala Steward on an old repo is likely to generate a lot of PRs! The number of PRs gives an idea of the state of the codebase. Don't be overwhelmed by it. Work out a strategy for prioritising updates. This might be picking off the PRs that only affect the test scope first and then patches, or focusing on a certain technology area first, or looking for redundant dependencies that could be removed first. Have a daily target of reducing the number of PRs to a certain level.

Integration steps

To integrate Scala Steward into a Guardian repo:

  1. If the repo is public, add it to our list of public repos. Otherwise, if it's a private repo, add it to the list of private repos. These are separate because GHA workflow runs in public repos are free of charge.
  2. If the repo needs custom configuration, add a config file to its root. (Example). In most cases you won't need to do this but have a look over the available options.
  3. GHA workflow runs are scheduled in the public and private repos. Have a look over the output of the first run of the workflow after your repo has been added to make sure it is successful. (Example)

Grouping dependency updates

It can be convenient to merge all the recent updates in a single PR depending on your team practices.
To do this, add a pullRequests.grouping section to the custom config in your repo. (Example). Instructions on how to group updates together are here.
Partitioning PRs into a group for frequently updating repos and another group for everything else is under consideration in the central configuration for the Guardian's Scala-Steward managed repos.

Dependency migrations

When the group or artifact ID of a dependency changes, it's possible to configure Scala Steward to continue updating it using the new name. This feature is documented here.

Things to consider

  • You'll still need to monitor your dependencies to see if any have been abandoned or are no longer in use by the codebase.
  • The effect of an updated dependency on transitive dependencies can often be surprisingly extensive, occasionally even if it's only a patch update.
  • As well as a set of tests that give you confidence at compile time, you'll also need to inspect the effect of the update on runtime behaviour, either by automated or manual smoke tests. You'll save yourself a lot of pain if you can do this in an automated way.

It has to be said that all these factors to take into account would apply regardless of the mechanism you used to manage your dependencies.

Footnotes

  1. Dependency injection is a software design pattern that implements inversion of control between a client and a service for resolving dependencies. The term injection refers to a third party (named the injector) which is responsible for constructing the services and injecting them into the client. Usual injectorsare dependency injection frameworks such as guice, dagger, macwire, spring.

    Most of the time, dependency injection does not solve a real business problem. You don't need to have a single place where you define which concrete instance your trait should be used. This is accidental complexity, as coined by Fred Brooks in No Silver Bullet

    With The play framework you should always use compile-time dependency injection which refers to an object oriented way to specify your components declaratively in scala.