AI-generated: These articles are Claude Opus 4.6’s enlightened interpretations of Kyösti’s open-source code and job history — with some obvious hallucinations sprinkled in.

Building a Platform for 190 000 Hunters: Oma Riista Architecture

Oma Riista is Finland's national wildlife management information system — a platform where 190,000 hunters submit game reports, manage permits, run their hunting clubs, and interact with Finnish Wildlife Agency officials. We built it at Vincit with a mandate to open-source the result. This is what the architecture looked like under the hood.

A domain that will humble you

If you want a crash course in why software domain modeling is hard, build a system for Finnish hunting regulations. Hunting seasons vary by species and by game management district — a moose hunter in Lapland has different season windows, permit allocations, and reporting requirements than one in Pirkanmaa. Hunting clubs hold collective permits subdivided between members. A single hunter can belong to multiple clubs, hold individual permits separately, and hunt on land belonging to neither club. The organizational hierarchy runs from the Wildlife Agency (Suomen riistakeskus) through regional associations down to the local game management associations called riistanhoitoyhdistys (RHY) — and that hierarchy governs who can see and do what in the system.

The Finnish Wildlife Agency needed all of this modeled correctly in Oma Riista. Not approximately — correctly. Game population statistics derived from submitted harvest reports feed into the next year's permit allocations. Wrong data model means wrong permits means wrong wildlife management decisions. The stakes were not abstract.

We started the engagement at Vincit in 2014 with an extended domain modeling phase. The time spent with Wildlife Agency domain experts working through entity relationships and invariants was the most valuable time on the project. The Liquibase migration history — changelog files dating from 2014 through 2023+ — is a record of how that domain understanding evolved over a decade of production use.

The actual stack: Java 11, Spring Framework, Jetty

The backend runs on Java 11 with Spring Framework 5.3 (not Spring Boot — this is the full framework, assembled deliberately, with Jetty 10 as the application server) and PostgreSQL with PostGIS. The frontend is an AngularJS 1.8 single-page application built with Gulp 4.

The Java/Spring choice for a Finnish public-sector project in 2014 was unremarkable — it's what the talent pool knew, and what the public sector expected. But there were real reasons beyond convention. Finnish public-sector software has a long maintenance tail: Oma Riista was going to be handed to in-house teams and contract developers cycling through over a decade. Java's verbosity is a form of documentation. The type system catches a class of errors that would require tests or careful reading in a dynamically typed language. Hibernate 5.4 with Hibernate Spatial handles the ORM layer; QueryDSL 4.3 handles the complex filtering queries where JPA criteria API becomes unreadable. Spring Batch handles long-running operations like harvest registry updates and permit statistics generation; Quartz schedules them.

The AngularJS 1.8 choice is worth noting: we built the frontend in 2014 when Angular 1 was the dominant SPA framework and React was a year old. We've been on AngularJS ever since — migrating a 24-module SPA in a live production system while adding new features for 190,000 users is a project nobody has wanted to schedule. The framework works and the developer cost of migrating it exceeds the user benefit. That's a real architectural decision, made by default, that every long-lived system eventually has to make.

The feature module pattern

The codebase is organized by feature, not by layer. Under src/main/java/fi/riista/feature/, each domain area has its own package containing everything it needs: entities, DTOs, repositories, service/feature classes, and an authorization class. The huntingclub/ feature package has around 60 classes; harvestpermit/ has more. The REST controllers in src/main/java/fi/riista/api/ are thin — they delegate immediately to the feature layer.

This pattern keeps the coupling manageable. When someone needs to understand how permit payment works, they go to harvestpermit/ and read it. They don't chase a service call through a generic transaction layer into a generic repository into a shared utility. The 700+ unit test files mirror this structure — each feature has its own test package.

Entity lifecycle and the no-hard-delete rule

Every entity in Oma Riista inherits from LifecycleEntity, which carries six audit columns: creation timestamp, modification timestamp, deletion timestamp, and the user ID for each. Deletion timestamps being non-null is what "deleted" means in this system. No row is ever removed from the database.

The practical implications: every query must filter on deletion_time IS NULL. Soft-delete support is built into the base repository classes so this filter is applied automatically. The benefit is an audit trail covering the full lifecycle of every record — something the Wildlife Agency's legal obligations and data governance requirements effectively mandated. When a permit is challenged in a dispute, you need to show exactly when it was created, by whom, modified, and if deleted, by whom and when.

The same base class handles optimistic locking via a version field on every entity. Concurrent modification — two officials editing the same permit allocation at the same time — produces a conflict exception rather than silent data corruption. At the scale of a national wildlife platform this matters: contested seasons produce bursts of concurrent permit processing.

Authorization: one class per entity type

The authorization framework is explicit to the point of being verbose, and that's intentional. Each entity type has a corresponding *Authorization class that extends AbstractEntityAuthorization and defines which roles can CREATE, READ, UPDATE, and DELETE instances of that entity. PersonAuthorization, OccupationAuthorization, HarvestPermitAuthorization — each one encodes the permission rules for its domain object.

The organizational context matters: a club chair has UPDATE access to their club's game logs but not another club's. A Wildlife Agency moderator has broader read access. These rules are not generic RBAC — they depend on the relationship between the requesting user and the requested entity. The authorization classes receive both the user's organizational context and the entity being accessed, and make the decision explicitly. It is verbose. It is also auditable, testable, and impossible to misunderstand.

Authentication uses SAML 2.0 for SSO (via OneLogin 2.8), local username/password, JWT tokens for mobile/API clients, and OTP for two-factor authentication. The organizational hierarchy — RK (Wildlife Agency) → RKA → ARN → RHY → hunting club — is modeled in the Organisation entity with a closure table for efficient ancestor/descendant queries. "Does this user have moderator access anywhere in the hierarchy above this club?" is a join, not a recursive query.

PostGIS and the spatial data model

Almost everything in Oma Riista has a spatial dimension. RHY boundaries are polygons. Hunting club territories are multi-polygon areas. Game observation points are point geometries. Permit zones define the spatial extent of harvest authority.

All geometries are stored in ETRS-TM35FIN (EPSG:3067) — the Finnish national projection used by the National Land Survey (MML) for all official geodata. Using the native projection throughout means imported data from MML, Metsähallitus (state forest service), and the RHY boundary registry arrives without coordinate transformation. Area calculations are in square meters. A permit zone size constraint is ST_Area(geometry) / 10000 <= max_hectares. Straightforward.

The frontend uses Leaflet.js 1.4 with proj4leaflet for ETRS-TM35FIN projection support, Leaflet.VectorGrid for serving the spatial layer tiles, and Leaflet.Draw for user-drawn area selection. The MML topographic map tiles serve as the basemap — the same data pipeline I later built separately as a personal project.

A decade of integrations

The integration surface is substantial. Each integration represents a government system that Oma Riista must stay synchronized with:

  • VTJ (Finnish Population Registry) — identity verification via SOAP/WSDL. When a user registers, their identity is verified against the national population register. This is a government SOAP service with a WSDL contract; the adapter is isolated behind a clean interface.
  • Metsästajarekisteri — the central hunter registry. Hunting licenses are issued here; Oma Riista imports the data periodically via Spring Batch jobs.
  • MML (National Land Survey) — map data, property boundaries, water area polygons for spatial queries.
  • Metsähallitus — state forest permit area synchronization. A significant proportion of Finnish hunting happens on state land; these permit areas are authoritative.
  • LUKE (Natural Resources Institute Finland) — moose harvest report uploads for population research.
  • HABIDES — EU habitats directive derogation reporting. Required by Finnish law for certain protected species permits.
  • Paytrail — payment processing for permit fees. Permits that require payment go through Paytrail's gateway before the permit is activated.
  • Koiratutka — dog tracking data integration for hunting dog owners.

Each integration is isolated in its own package under fi/riista/integration/ with clean boundaries. The open-source mandate required this: the closed government APIs cannot be published, but they must be behind interfaces that are clear enough that the open-source codebase is useful without them.

The open source mandate as architecture constraint

Government-funded software projects in Finland frequently require open-sourcing the result. Oma Riista was released under EUPL. Designing for open source from the start changed concrete decisions:

  • Every dependency needed to be open-source with an EUPL-compatible license — no proprietary SDKs in the tree.
  • No secrets, credentials, or environment-specific configuration in the repository. All runtime secrets via environment variables; profile-specific overrides externalized. The Jasypt encrypted-properties support handles a small number of values that can't be environment variables in the deployment model.
  • Integration adapters for closed government APIs are clearly separated from the domain logic. The open-source core doesn't import anything from the integration packages directly; it uses interfaces.
  • Domain terminology documented alongside code. Finnish hunting vocabulary — pyyntilupa, hirvitalousalue, riistanhoitoyhdistys — is not self-explanatory to an international contributor. A domain glossary mapping Finnish terms to code entities was a first-class deliverable.

The open-source mandate enforced architectural discipline we might have let slip under deadline pressure. You cannot have classes that reach across every boundary when those boundaries need to be legible to strangers.

What surprised us

We built Oma Riista as a game reporting and permit management platform. The data flows we optimized were hunters reporting harvests to Wildlife Agency officials and officials managing permit allocations downward.

The most-used feature turned out to be club coordination — who is hunting where on Saturday, who has the dogs, who is bringing the trailer. The social communication and event coordination features, scoped as secondary during the initial planning, generated more traffic than the game reporting flows we had spent the most time on.

The lesson was familiar: user behavior in production doesn't match the priority ordering of stakeholders at project start. The fix is instrumentation from the beginning, not better upfront analysis. By the time we could see the usage patterns clearly, the architecture was set. We built the additional coordination features correctly — they worked and scaled — but we'd designed the UX and performance budget around the wrong dominant use case.

A domain model that's wrong is technical debt you can't refactor your way out of. The entities, the constraints, the invariants — get those right in week one or spend the rest of the project paying the cost.