Botanical SaaS — all-in-one¶
This page is assembled from compact project documentation sections. Individual section files remain the source of truth; this page is for sequential reading, review, and PDF-style export.
Contents¶
- Summary
- Overview
- Context and Problem
- Goals, Requirements, and Constraints
- Role and Responsibilities
- System Model
- Architecture and Integrations
- Security, Quality, and Operations
- Decisions, Trade-offs, and Risks
- Roadmap and Demonstration
- Architecture Decision Records
Summary¶
Status¶
MVP / pilot run
Role¶
Co-founder, technical architecture owner, system designer
Stack¶
Java 21, Spring Boot, PostgreSQL/PostGIS, MinIO/S3-compatible storage, Angular, OpenLayers, Docker
Project value¶
For the target audience (botanical gardens, nurseries, plant breeders, holders of scientific collections and private plant collections):
- transition from disconnected Excel/local databases to a unified web platform;
- a unified taxonomic layer as the foundation for comparable collection data;
- managed data disclosure, public pages, and QR scenarios;
- a foundation for network exchange scenarios between organizations.
For the professional profile:
- designing and implementing SaaS from domain idea through MVP and pilot launch;
- working with multi-tenancy, RBAC, domain data model, API, GIS, AI-assisted import, public data preparation, and legislative compliance (152-FZ);
- deliberate and economical use of AI without delegating architectural decisions to the model.
What this demonstrates¶
This project demonstrates my ability to:
- work at the intersection of system analysis, system design, implementation, and deployment.
- take an idea through to a running system with multiple availability zones and a scaling roadmap
- strategically plan implementation of a small-to-medium-scale SaaS
- safely and in a controlled way apply LLM code generation for feature implementation
Overview¶
Botanical SaaS is a multi-tenant SaaS platform for botanical gardens, nurseries, plant breeders, scientific collections, and private plant collections.
The system replaces disconnected Excel files, local databases, and legacy desktop tools with a unified web platform for managing living plant collections, scientific taxonomy, geospatial data, public organization pages, QR labels, import, lists, and controlled data exchange.
My responsibility included domain modeling, backend architecture, access model design, data structure and API design, technical documentation, backend implementation, and preparing the system for cloud-ready deployment.
Context and Problem¶
Context¶
The target audience already has data, but it is scattered across paper journals, Excel, Word/PDF, Access/FileMaker, local databases, legacy desktop systems, and partially across specialized solutions. The project therefore does not sell "tracking from scratch"; it sells a transition from a chaotic, incomparable, and poorly publishable state into a structured system.
For botanical gardens, a rough estimate is: Excel remains the main operational layer for 35–50%, paper is significant for 10–25%, local databases and desktop systems for 20–30%, modern web/cloud systems for 5–15%. Specialized systems such as IrisBG, BG-BASE, BRAHMS, and Hortis serve the professional upper market but do not dominate globally.
BGCI is not a direct competitor as an operational collection management system. BGCI matters as a global reference and aggregation layer: GardenSearch shows organizations, PlantSearch aggregates taxon-level data from collections. But it does not replace day-to-day tracking of specimens, movements, losses, photos, GIS, QR codes, internal roles, import, and reporting.
The picture varies by segment. Private collectors mostly use Excel, Google Sheets, paper, Notion/Airtable, or simple hobby tools. Plant breeders track crosses, seedlings, lines, and trials, often in Excel or internal databases. Cultivar registrars work with spreadsheets, PDFs, local databases, and public registers. Museums are more digitized in preserved collections, but living collections are often tracked similarly to botanical gardens. Nurseries more often have ERP/web tracking, but their focus is warehouse, sales, and batches—not a scientific collection.
The main market pain is data incomparability. In Excel, the same taxon may be recorded in Latin, with author, without author, in Russian, via a synonym, with a typo, or under an old name. Automatic exchange, search, reporting, and collection comparison are therefore nearly impossible without a unified taxon layer.
Problem¶
Botanical collections are often maintained in disconnected spreadsheets, local databases, and institution-specific documents. This makes it difficult to:
- keep plant records consistent;
- link plants to verified scientific taxonomy;
- track plant locations and changes in their condition;
- manage access across departments and organizations;
- import historical data safely;
- publish selected collection data publicly;
- exchange information between institutions and collectors (seed exchange to support biodiversity).
Botanical SaaS addresses this as a unified operational platform for living plant collections.
Goals, Requirements, and Constraints¶
Goals and Non-Goals¶
Primary project goals¶
- Move living plant collection tracking from Excel, paper journals, and local databases into a unified web system.
- Ensure scientifically correct linkage of records to a global taxonomy source, cultivars, and grexes.
- Give botanical gardens, collectors, museums, and nurseries a tool for tracking specimens, locations, photos, statuses, accessions, and removals.
- Implement a GIS core: site map, areas, greenhouses, beds, plant points, and placement polygons.
- Simplify migration of existing data through Excel import, column mapping, name validation, and error reports.
- Create a public showcase for organizations: garden pages, public lists, plant cards, and QR labels.
- Support selective data disclosure.
- Build a network layer for the botanical community: exchange lists, wishlists, and automatic matching of desired taxa with available material.
- Give administrators and curators reporting on collection holdings, departments, taxa, accessions, and removals.
- Form a foundation for the long-term cultivar, registrar, and industry data standardization contour.
- Support generation of QR labels linking to a plant card in the system.
Out of scope¶
- Not a replacement for BGCI as a global botanical garden directory and taxon-level data aggregator.
- Not a full-cycle herbarium system for herbarium collections or museum holdings.
- Not an ERP for commercial nurseries focused on sales, warehouse, procurement, production, POS, and accounting.
- Not a CRM, task tracker, document management system, or corporate portal.
- Does not include physical printing, production, installation, or maintenance of QR labels on site.
- Does not replace official ICRA/registrar legal cultivar registration; the system can only support a digital data and application contour.
- Does not guarantee automatic cleanup of all historical data without expert involvement: disputed names, synonyms, varieties, and errors require validation.
- Does not expose private collections by default; data publication remains an owner-controlled decision.
- Not a general-purpose BI/analytics platform; reporting is limited to living collections, accessions, and plant removals.
- Does not include a heavy enterprise contour at the first stage: billing, SLA, multi-region replication, public procurement, software registry, custom integrations, or separate installations per client.
Requirements¶
Business Requirements¶
-
BR-001. Centralize living plant collection tracking.
The project must provide botanical organizations, collectors, and adjacent market participants with a unified web system to replace disconnected Excel files, paper journals, Word/PDF documents, and local databases. -
BR-002. Improve scientific correctness of plant data.
The project must ensure linkage of plant records to an authoritative taxonomic base, support the hierarchy family → genus → species, and a separate contour for cultivars and grexes. -
BR-003. Reduce cost and complexity of migrating existing collection data.
The project must allow import of historical data from Excel and other tabular sources with column mapping, value normalization, name validation, and error reports. -
BR-004. Support operational tracking of plant specimens.
The project must support plant instance cards with inventory numbers, statuses, provenance, acquisition form, placement location, photos, custom fields, and change history (full attribute list agreed separately). -
BR-005. Link collection data to spatial context.
The project must let organizations map territory, departments, areas, greenhouses, beds, and individual plants using points, polygons, and an interactive map. -
BR-006. Give organizations a public digital showcase.
The project must allow creation of public organization pages, public plant cards, public lists, and a collection map without building a separate website. -
BR-007. Link physical plants to digital cards.
The project must support generation of QR labels leading a visitor or staff member to a public or internal plant card. -
BR-008. Support managed data disclosure.
The project must let collection owners control data visibility: private, for registered users, for the community, or public. -
BR-009. Create a network exchange layer between botanical community participants.
The project must support collection lists, exchange lists, and wishlists so participants can match desired taxa with available material at other organizations, enabling exchange. -
BR-010. Provide reporting for curators and administration.
The project must provide reports on collection composition, taxa, departments, accessions, removals, lists, and collection holdings status.
... The public version includes a shortened requirements fragment. Full detail is not disclosed due to volume and product constraints.
Rules and Constraints¶
Business Rules¶
-
RULE-001. Organization data is closed by default.
Collection data for a new organization is considered private until the owner explicitly changes the visibility level. -
RULE-002. The data owner controls publicity.
The organization independently determines which plants, lists, photos, coordinates, and pages are available publicly, to the community, or only to internal users. -
RULE-003. A user cannot access another organization's data without permission.
Access to plants, lists, places, photos, and import must be limited by the user's organization and assigned roles. -
RULE-004. Species records must be linked to reference taxonomy.
Users must not create arbitrary species records without a link to an authoritative taxonomic reference. -
RULE-005. Local cultivars belong to the creating organization.
A cultivar created within an organization is considered local and does not become globally available automatically (only through registration by an authorized registrar).
... The public version includes a shortened business rules fragment. Full detail is not disclosed due to volume and product constraints.
Constraints¶
-
CON-001. The system must be a web/SaaS solution.
The product is designed as a web platform with centralized access, not as a desktop application. -
CON-004. Excel must be supported as the primary migration format.
The system must account for the fact that source data for the target audience is most often in Excel files. -
CON-005. Source data may be dirty and heterogeneous.
Import must account for typos, synonyms, local names, incomplete values, non-standard columns, and historical tracking formats. -
CON-006. GIS accuracy is limited by source data quality.
The system can store and display points and polygons, but actual accuracy depends on user actions and field data quality.
... The public version includes a shortened constraints fragment. Full detail is not disclosed due to volume and product constraints.
Role and Responsibilities¶
My Role¶
I acted as a co-founder and technical owner of the system architecture.
My work included:
- translating domain expert knowledge into a structured SaaS model;
- designing backend architecture, API, data structures, and domain model;
- implementing backend functionality and integration logic;
- designing RBAC with organization (tenant) context access rights;
- modeling plant instances, taxonomy, places, lists, photos, imports, and public pages;
- preparing architecture documentation, C4 diagrams, and ADRs;
- aligning technical decisions with a domain expert;
- using AI-assisted development tools to accelerate routine implementation while keeping manual control over architecture, data model, API contracts, review, and deployment decisions.
AI Usage¶
The project was developed with AI assistance.
LLMs were used to accelerate routine implementation, generate boilerplate code, and iterate quickly. Key decisions remained under manual control:
- requirements interpretation;
- domain modeling;
- architecture decisions;
- data boundaries;
- access model;
- code review;
- debugging;
- deployment decisions;
- technical documentation.
System Model¶
Domain Model¶
Scientific Taxonomy¶
Plant records are tied to a centralized taxonomic layer (not disclosed publicly).
The model supports families, genera, species, cultivars, grexes, global taxa, and tenant-owned local cultivated entities.
Data Model¶
High-Level Data Model¶
erDiagram
ORGANIZATION ||--o{ ORG_UNIT : contains
ORGANIZATION ||--o{ PLANT : owns
ORGANIZATION ||--o{ PLACE : manages
ORGANIZATION ||--o{ PLANT_LIST : maintains
ORGANIZATION ||--o{ PUBLIC_PAGE : publishes
ORG_UNIT ||--o{ PLANT : curates
PLACE ||--o{ PLACE : contains
PLACE ||--o{ PLANT : locates
PLANT }o--|| SYSTEM_TAXON : "required taxon_id"
PLANT ||--o{ PHOTO : documents
PLANT }o--o{ PLANT_LIST : included_in
SYSTEM_TAXON ||--o| GLOBAL_TAXON : "species subtype"
SYSTEM_TAXON ||--o| CULTIVATED_ENTITY : "cultivar/grex subtype"
PUBLIC_PAGE ||--o{ PLANT_LIST : exposes
PUBLIC_PAGE ||--o{ PLANT : exposes_selected
ORGANIZATION {
uuid id PK
string public_name
string slug
string visibility
}
ORG_UNIT {
uuid id PK
uuid organization_id FK
uuid parent_id FK
string name
}
PLANT {
uuid id PK
uuid organization_id FK
uuid org_unit_id FK
uuid place_id FK
uuid taxon_id FK "mandatory"
string accession_number
string individual_code
string status
string provenance_type
geometry point
jsonb custom_fields
}
SYSTEM_TAXON {
uuid id PK
string display_name
string normalized_name
string taxon_type
string rank
}
GLOBAL_TAXON {
uuid id PK
uuid system_taxon_id FK
string id
string ipni_id
string family
string genus
string species_epithet
string scientific_name
}
CULTIVATED_ENTITY {
uuid id PK
uuid system_taxon_id FK
string genus
string cultivar_or_grex_name
string breeder_name
string registrar_name
int registration_year
string visibility_scope
}
PLACE {
uuid id PK
uuid organization_id FK
uuid parent_id FK
string name
geometry point
geometry polygon
}
PLANT_LIST {
uuid id PK
uuid organization_id FK
string name
string list_type
string visibility
}
PHOTO {
uuid id PK
uuid PLANT_id FK
string caption
string photo_tag
boolean publication_allowed
}
PUBLIC_PAGE {
uuid id PK
uuid organization_id FK
string slug
boolean visible
} Key Model Idea¶
The central entity of the system is Plant, a digital twin of a specific plant in a living collection. This is not merely a reference row or an abstract species, but an accession record for a specific specimen: with inventory number, status, placement location, organization ownership, photos, lists, and public representation.
The mandatory attribute taxon_id is the core of the model. It links each plant instance to the unified root entity SystemTaxon. Thanks to this, all operational scenarios—tracking, import, search, lists, exchange, QR pages, reporting, and the public map—work not with an arbitrary text plant name, but with a stable taxon identifier.
SystemTaxon acts as the common root entity for two major taxonomic contours:
- globalTaxon — species-level taxa from an authoritative catalog.
- CultivatedEntity — cultivars and grexes that may be recognized global records from registrars, or local records created within a specific organization.
This approach lets the user select a plant from a unified taxon lookup without artificially separating species search from cultivar search. For the user it looks like a single plant reference; internally the system preserves the distinction between scientific species taxa, global cultivars, grexes, and organization-local cultivars.
The architectural value is that all collection data becomes comparable across organizations. The same taxon_id can be used in plant cards, collection lists, wishlists, exchange lists, public pages, and reports. This creates a foundation for network scenarios: the system can match which taxa one organization seeks and another is ready to transfer, without relying on unstable text names, synonyms, local variants, and typos.
In the portfolio the model is shown at a high level. Internal auxiliary entities, visibility mechanisms, access tables, cultivar globalization workflows, and implementation details are intentionally omitted.
API Contracts¶
Endpoint names and attributes are intentionally altered for the public space.
API Design Approach¶
The backend exposes a REST/JSON API for managing living plant collections in a multi-tenant SaaS environment. API contracts are organized around domain capabilities: plant inventory, taxonomy lookup, batch operations, import workflows, public pages, media access and exchange scenarios.
Security is enforced at several layers:
- JWT-based authentication through Spring Security.
- Method-level authorization for sensitive operations.
- Tenant isolation through organization/root tenant context.
- Data-level filtering by tenant ownership.
- Bean Validation for request DTOs.
- Controlled public DTOs for anonymous endpoints.
- Soft delete for recoverable destructive actions.
- Centralized error handling with standard HTTP statuses.
1. Search Plant Instances¶
Purpose¶
Retrieve a paginated list of plant instances available to the current user within the active organization context.
Contract¶
GET /api/inventory/plants
Query Parameters¶
keyword optional text search
organization optional tenant/root context
unit optional department or collection unit
list optional list filter
recursive optional flag for nested units
filter optional faceted filters
page pagination parameter
size pagination parameter
sort sorting parameter
Response¶
{
"items": [
{
"id": "uuid",
"displayName": "string",
"taxon": "summary",
"inventoryCode": "string",
"status": "string",
"location": "summary",
"visibility": "string"
}
],
"page": {
"number": 0,
"size": 25,
"totalElements": 0
}
}
Security Notes¶
- Requires authenticated user.
- Result set is restricted to the tenant context available to the current user.
- Query specifications apply tenant filtering before returning data.
- Filter expressions are parsed into allowlisted criteria rather than executed as raw dynamic queries.
- Pagination and sorting use framework-level pageable parameters.
2. Get Plant Instance Details¶
Purpose¶
Retrieve a single plant instance as a digital record of a real plant in a living collection.
Contract¶
GET /api/inventory/plants/{plantId}
Response¶
{
"id": "uuid",
"taxon": {
"id": "uuid",
"displayName": "string",
"type": "species | cultivar | grex"
},
"inventory": {
"accessionNumber": "string",
"individualCode": "string",
"status": "string"
},
"organization": "summary",
"location": "summary",
"photos": ["summary"],
"customFields": {}
}
Security Notes¶
- Requires authenticated user.
- Access is checked against the plant’s tenant ownership and the user’s organization memberships.
- Internal fields are not exposed through public-facing contracts.
- Non-existing or inaccessible resources are returned through standard error responses.
3. Create Plant Instance¶
Purpose¶
Create a new plant instance in an organization’s living collection.
Contract¶
POST /api/inventory/plants
Request¶
{
"taxonId": "uuid",
"organizationUnitId": "uuid",
"placeId": "uuid",
"inventoryData": {},
"status": "string",
"customFields": {}
}
Response¶
201 Created
{
"id": "uuid",
"displayName": "string",
"taxon": "summary",
"inventoryCode": "string",
"status": "string"
}
Security Notes¶
- Requires authenticated user.
- Requires administrative or editor-level access to the target organizational unit.
- Request body is validated before domain processing.
taxonIdis mandatory and must refer to an allowed species, cultivar or grex visible in the current tenant context.- Creation is delegated to the service layer after authorization and validation.
4. Update Plant Instance¶
Purpose¶
Update an existing plant instance, including inventory metadata, taxon reference, status or organizational placement.
Contract¶
PUT /api/inventory/plants/{plantId}
Request¶
{
"taxonId": "uuid",
"organizationUnitId": "uuid",
"placeId": "uuid",
"inventoryData": {},
"status": "string",
"customFields": {}
}
Response¶
{
"id": "uuid",
"displayName": "string",
"taxon": "summary",
"status": "string",
"location": "summary"
}
Security Notes¶
- Requires authenticated user.
- Requires write access to the existing plant record.
- If the plant is moved to another organizational unit, access to both the current and target contexts must be validated.
- Invalid move attempts are rejected with an access error.
- Request validation is performed through typed request DTOs and Bean Validation.
5. Soft Delete Plant Instance¶
Purpose¶
Move a plant record to trash without physically deleting it from the database.
Contract¶
DELETE /api/inventory/plants/{plantId}
Response¶
204 No Content
Security Notes¶
- Requires authenticated user.
- Requires administrative access to the plant’s organizational context.
- Deletion is recoverable by default.
- Soft-deleted records are excluded from normal queries.
- Permanent deletion is restricted to elevated platform-level roles.
6. Batch Operations on Plant Instances¶
Purpose¶
Perform bulk actions on selected plant instances: move, update, clone, generate label data or mark labels as printed.
Contracts¶
POST /api/inventory/plants/batch/move
POST /api/inventory/plants/batch/update
POST /api/inventory/plants/batch/clone
POST /api/inventory/plants/batch/label-data
POST /api/inventory/plants/batch/mark-printed
Request¶
{
"plantIds": ["uuid"],
"operationPayload": {}
}
Response¶
{
"successCount": 0,
"failureCount": 0,
"results": [
{
"id": "uuid",
"status": "success | failed",
"message": "string"
}
]
}
Security Notes¶
- Requires authenticated user.
- Each affected plant must be checked against tenant ownership and user permissions.
- Batch operations should not bypass per-resource authorization.
- Partial success is supported for operations where some records may fail validation or authorization.
- Input collections are validated before processing.
7. Dictionary Lookup for Plant Forms¶
Purpose¶
Return controlled dictionary values used by plant forms, such as lifecycle states, growing conditions or acquisition forms.
Contract¶
GET /api/inventory/dictionaries/{dictionaryType}
Response¶
[
{
"id": "string",
"code": "string",
"label": "string"
}
]
Security Notes¶
- Requires authenticated user for internal dictionaries.
- Dictionary responses contain only safe reference data.
- Values are controlled by the platform or tenant configuration, not arbitrary free text.
8. Taxon Lookup¶
Purpose¶
Provide a unified lookup over species, cultivars and grexes when creating or editing a plant instance.
Contract¶
GET /api/taxonomy/search
Query Parameters¶
query search term
type optional taxon type filter
page pagination parameter
size pagination parameter
Response¶
{
"items": [
{
"id": "uuid",
"displayName": "string",
"taxonType": "species | cultivar | grex",
"source": "reference | global | local"
}
]
}
Security Notes¶
- Global reference species are available across tenants.
- Global cultivated entities are available across tenants.
- Local cultivated entities are visible only to allowed tenant contexts.
- The API hides internal visibility mechanics and exposes only selectable taxon summaries.
9. Smart Import Workflow¶
Purpose¶
Import existing collection data from spreadsheets through a staged workflow.
Contracts¶
POST /api/import/sessions
POST /api/import/sessions/{sessionId}/sheet
POST /api/import/sessions/{sessionId}/mapping
POST /api/import/sessions/{sessionId}/resolve
POST /api/import/sessions/{sessionId}/execute
GET /api/import/sessions/{sessionId}/status
GET /api/import/sessions/{sessionId}/error-report
Workflow¶
Upload file
→ Select sheet
→ Map columns
→ Resolve values
→ Confirm uncertain matches
→ Execute import
→ Review results
Response Example¶
{
"sessionId": "uuid",
"status": "uploaded | mapped | resolving | ready | executing | completed | failed",
"progress": {
"processedRows": 0,
"totalRows": 0
}
}
Security Notes¶
- Requires authenticated user.
- Requires tenant context and import permission.
- Uploaded files are stored outside the relational database.
- Import sessions are tenant-scoped.
- Ambiguous matches require user confirmation.
- Invalid rows are reported without blocking all valid rows.
- Background jobs are used for long-running import stages.
10. Public Plant Page¶
Purpose¶
Expose a safe public view of a plant record, commonly used as a QR label target.
Contract¶
GET /api/public/plants/{publicSlug}
Response¶
{
"displayName": "string",
"taxon": "public summary",
"organization": "public summary",
"photos": ["public media reference"],
"publicDescription": "string"
}
Security Notes¶
- Does not require authentication.
- Returns only public DTOs.
- Excludes internal identifiers, tenant metadata, private notes, internal inventory fields and restricted photos.
- Requires public visibility settings on the owning organization and plant/list context.
- Media access uses short-lived signed URLs or equivalent controlled delivery.
11. Exchange Match Lookup¶
Purpose¶
Find potential exchange matches between one organization’s wishlist and other organizations’ shared exchange lists.
Contract¶
GET /api/exchange/matches/{wishlistId}
Response¶
{
"wishlist": "summary",
"matches": [
{
"taxon": "summary",
"sourceOrganization": "public or shared summary",
"availableMaterial": "summary",
"visibility": "string"
}
]
}
Security Notes¶
- Requires authenticated user.
- User must have access to the source wishlist.
- Candidate matches are limited to data explicitly shared for exchange or visible to the relevant community scope.
- Matching is based on taxon identifiers rather than raw text names.
- Private collection data is not disclosed through the exchange API.
Error Handling Pattern¶
{
"status": 400,
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": []
}
Typical response statuses:
200 OK successful read or update
201 Created resource created
204 No Content successful deletion
400 Bad Request invalid input or domain validation failure
403 Forbidden authenticated but not allowed
404 Not Found resource missing or not visible
409 Conflict state conflict or resource in use
429 Too Many Requests rate limit exceeded
Security Summary¶
The API design combines framework-level and domain-level protection:
- Spring Security authenticates requests and forms user context.
- JWT carries identity; roles and permissions are resolved server-side.
- Method-level checks protect write operations and administrative actions.
- Tenant-scoped queries prevent data leakage between organizations.
- DTO validation blocks invalid requests before business logic execution.
- Public endpoints use separate DTOs with a restricted field set.
- Soft delete protects against accidental data loss.
- Platform-level operations are separated from tenant operations.
- Audit and login events provide traceability for sensitive actions.
Architecture and Integrations¶
Architecture¶
Container diagram (C4 Container)¶
C4Container
title Container Diagram for Botanical SaaS MVP
Person(user, "User")
System_Boundary(sys, "System") {
Container(spa, "Single Page Application", "Angular 21, OpenLayers", "User interface")
Container(static, "Frontend Static", "nginx", "Container for storing<br/> static frontend files")
Container(backend, "Backend Business Logic", "Spring Boot", "Business logic")
ContainerDb(reldb, "Relational DB", "PostgreSQL + PostGIS", "Plant instances, lists,<br/> users, RBAC roles")
ContainerDb(objStore, "Object Storage", "MinIO", "Photos and multimedia.<br/> Collection import files")
}
Boundary(ext, "External systems", "") {
Container_Ext(global, "global", "Taxonomic reference")
Container_Ext(vernacular, "WikiData", "Vernacular names reference<br/> for plants")
Container_Ext(llm, "LLM", "Intelligent species<br/> recognition")
}
Rel(user, spa, "Uses to<br/> manage plant collections")
Rel(spa, static, "Gets Angular<br/> static UI bundles", "HTTPS")
Rel(spa, backend, "Sends API calls", "HTTPS REST")
Rel(backend, reldb, "Reads/writes data", "SQL")
Rel(backend, objStore, "Uploads/reads media", "S3 API")
Rel(backend, global, "Requests taxa/cultivars,<br/> writes cultivars", "HTTPS/REST")
Rel(backend, vernacular, "Gets species names<br/> in national languages", "HTTPS/REST")
Rel(backend, llm, "Uses LLM API<br/> for species name suggestions", "HTTPS/OpenAI API compatible")
UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="3")
Integration Flows¶
Taxonomy catalog import¶
The system includes a manual mechanism for updating the internal taxon reference from an XLS export while preserving identifiers.
National taxon name enrichment¶
The system includes an automatic mechanism for enriching the internal vernacular plant names reference from open sources, subject to public API constraints.
Smart Import¶
The platform includes an XLS import wizard for migrating existing plant collections into the system.
The flow supports file upload, sheet selection, column mapping (including with AI assistance), value resolution, fuzzy matching, asynchronous processing, row-level results, and Excel export of erroneous rows.
For mapping column names to system entity attributes, and for more accurate recognition of species, cultivar, or enum values, LLM integration and a lightweight harness are provided. Recognition confirmation (when it was not 100%) is performed by the user.
Security, Quality, and Operations¶
Security and Access Model¶
Multi-tenancy and Access Control¶
The platform uses a soft multi-tenancy model based on a root organization unit pattern.
A user can belong to multiple organizations and have different roles depending on the current organization context. Access checks are applied at service, repository, API, and UI levels.
Public Showcase¶
Organizations can publish selected data through public pages, public plant cards, public lists, QR-label targets, and a global map.
The public layer uses separate DTOs and visibility rules to avoid exposing internal fields and tenant data.
Dual RBAC Contour¶
The system has a single User entity and two role-based access models: organizational roles and platform roles. One user may lead several organizations and also be a platform moderator; another user acts as a support engineer and therefore needs limited access to any tenant and object; a third user is a stakeholder with access only to dashboard modules. All three users authenticate through a unified authorization and authentication flow. Mechanism details are not disclosed.
Non-Functional Requirements¶
Key Quality Attributes¶
| Attribute | Scenario | Approach |
|---|---|---|
| Security | A user from organization A must not access organization B data | tenant-scoped queries, RBAC, public DTO, integration tests |
| Reliability | A stuck import must not block the system | background jobs, stale detection, retry/manual restart |
| Maintainability | The domain model changes actively at the MVP stage | modular monolith, package boundaries, DTO/service layers |
| Performance | Taxonomy reference search must remain acceptable as the catalog grows | indexes, normalized names, pagination, fuzzy matching strategy |
| Operability | A VPS failure must not lead to complete data loss | backups, restore plan, migration path |
Failure Modes¶
| Risk | Impact | Mitigation |
|---|---|---|
| tenant filtering error | data leakage | tests, AccessControlService, scoped queries |
| VPS failure | service unavailability | backup, restore plan, migration path |
| MinIO volume loss | loss of photos/imports | object storage backup |
| stuck import job | import blocked | job lifecycle, stale detection |
| public DTO leakage | exposure of internal data | separate DTOs, visibility rules |
Sizing and Cost Notes¶
Primary load drivers¶
- number of organizations;
- number of plant instances;
- number of photos per plant;
- Excel import size;
- Smart Import frequency;
- public traffic volume for maps, QR pages, and images;
- taxonomy reference volume.
Primary cost drivers¶
- VPS / compute;
- PostgreSQL/PostGIS storage;
- object storage for photos and imports;
- backups;
- LLM calls for Smart Import;
- public page and image traffic;
- monitoring and log storage.
Scaling tiers¶
See Roadmap and Demonstration.
Decisions, Trade-offs, and Risks¶
Key Decisions¶
Decoupled frontend and backend¶
The system uses a separate Angular frontend and Spring Boot backend API. This reduces coupling, allows independent evolution of UI and backend logic, and creates a foundation for future client channels.
Root-unit soft multi-tenancy¶
Each tenant is represented by a root organizational unit. Tenant-scoped entities carry root_unit_id, and access is constrained through repositories, specifications, services, and API-level checks.
Context-aware authorization¶
Permissions depend not only on the user's global role, but also on the current organization and selected organizational context.
This fits B2B scenarios where the same user may have different permissions in different organizations or departments.
PostGIS as part of the domain model¶
Plant locations, garden areas, greenhouses, beds, and polygons are modeled as spatial data, not as a secondary map overlay.
Hybrid current state and history model¶
The system separates current operational state from data related to change history and audit trail. This allows efficient work with current records while preserving traceability of important changes.
Controlled public data exposure¶
Public pages, plant cards, lists, photos, and map data are published through separate public endpoints and DTOs.
Visibility rules prevent accidental exposure of internal tenant data.
Architectural Trade-offs¶
1. Soft multi-tenancy instead of full isolation¶
Context¶
The system is designed for many organizations (multi-tenant). At an early stage the product needs low operational complexity, high development speed, and meeting functional requirements (taxonomic layer and ability to develop network scenarios between organizations).
Decision¶
Logical multi-tenancy through a root_unit_id attribute is used for data isolation. The root organizational unit is the organization boundary; all entities in the tree reference this root unit.
Rejected alternative¶
database-per-tenant or schema-per-tenant.
Rationale¶
- lower infrastructure complexity at an early stage
- allows absorbing organizations or spinning off departments with minimal entity migration
Trade-offs¶
- Data isolation is logical, not physical; any tenant filtering error in any API can break tenant isolation.
- Harder to back up, restore, export, and physically delete data for a single organization (right to be forgotten).
- Harder to dedicate a large client to separate infrastructure.
- Sharding and regional data separation will require additional design.
Compensating measures¶
- All entities belonging to an organization carry
root_unit_id. - Access is constrained at multiple levels: repository queries, JPA specifications, business-logic checks, controller method authorization.
- Central authorization is extracted into
AccessControlService. - Public APIs return only public DTOs and do not expose internal fields.
- Integration tests for cross-tenant access denial are written for critical scenarios.
- For 152-FZ compliance a separate availability zone for RF users is provisioned (two zones so far). Replication of public data between availability zones is planned, accounting for possible disconnection of the RF network segment from the global internet.
Review trigger¶
- enterprise clients requiring physical isolation
- data volume growth to noisy-neighbor levels
- need for regional data storage or legal requirement to physically separate organization data
2. Modular monolith instead of microservices¶
Context¶
The product has a broad domain model: plants, taxonomy, cultivars, lists, places, import, public pages, users, roles, media, and inter-organization exchange. The team is small and the domain model is still actively refined. It is also unclear how much load will differ across entities.
Decision¶
The backend is implemented as a modular monolith on Spring Boot: a single artifact with separation by domain areas through controllers, services, repositories, and DTOs.
Rejected alternative¶
A set of microservices: taxonomy service, collections service (+ import), media service, authorization and identity service, GIS service, public resources service.
Why this makes sense now¶
A monolith reduces distributed-system overhead: no network contracts between services, no distributed transactions, no service discovery infrastructure, no complex observability or inter-service failure orchestration. This speeds delivery and keeps the domain model cohesive while the product has not yet passed the first pilots.
Trade-offs¶
- Individual domain modules cannot be scaled independently.
- A failure in one module can affect the entire backend.
- Over time, implicit dependencies between domain areas may emerge.
- Import, media, and GIS may have different load profiles but still live in one application.
Compensating measures¶
- Strict package separation by domain areas. Service layer as the boundary for business logic, DTOs as the API boundary. This lowers the cost of extracting a bounded context into its own service.
- Asynchronous execution of heavy imports to avoid blocking a thread and hitting timeouts.
Review trigger¶
Service extraction makes sense when a specific module gets independent load scale, a dedicated owning team, a separate release cadence, or separate resilience requirements. First extraction candidates: import pipeline, media processing/storage gateway, public map/search read model.
3. Monorepo for backend and frontend instead of separate repositories¶
Context¶
The project is developed by a small team where one developer owns architecture, backend, frontend, deployment, and integration. For these conditions, speed of coordinated changes matters more than organizational independence of teams.
Decision¶
Backend and frontend are stored in one repository.
Rejected alternative¶
Separate repositories for backend, frontend, infrastructure, and documentation.
Why this makes sense now¶
A monorepo allows atomic API and UI changes, easier maintenance of full architectural context, faster full-stack refactoring and LLM code generation. The result is more stable because the model sees the connected product picture.
Trade-offs¶
- Responsibility boundaries may blur as the team grows.
- Harder to restrict access to parts of the codebase.
- Higher risk of broad changes without understanding blast radius.
Compensating measures¶
- Separate frontend/backend folders and independent build and run commands.
- Integration tests for frontend/backend interaction.
Review trigger¶
Separate repos become justified when independent teams with different release cycles appear, different code access policies, or need to publish parts of the system independently.
4. Docker Compose instead of cloud native¶
Context¶
At an early stage the system must deploy quickly on a VPS, demonstrate the product, run pilots, and keep infrastructure costs low.
Decision¶
Backend, frontend, PostgreSQL/PostGIS, and MinIO run in a single Docker Compose environment.
Rejected alternative¶
Full cloud-native infrastructure with dedicated DB and object storage capacity, container autoscaling, monitoring, and multi-zone deployment.
Why this makes sense now¶
Docker Compose gives fast cold start, reproducible environment, low cost, and a simple operational model. For MVP, demo stand, and early pilot this is better than premature cloud-native complexity.
Trade-offs¶
- A single VPS is a single point of failure.
- DB, object storage, and services compete for resources.
- Scaling is mostly vertical.
- No full high-availability model, no peak load handling. Cannot be considered production architecture.
- Backup, restore, and monitoring become critical operational tasks.
Compensating measures¶
- Services remain stateless.
- Configuration must be env-based.
- DB and object storage data live on persistent volumes with regular backups.
- Reverse proxy / TLS / rate limits are moved to the infrastructure layer.
- Target migration path must be described in advance: separate PostgreSQL/PostGIS instances, S3-compatible storage, horizontal scaling of service containers.
Review trigger¶
Transition is needed when paying clients appear, SLA expectations, growth in photo/import volume, high-availability requirements, regular VPS downtime, or need for regional data placement.
... Only part of the trade-offs is published in the portfolio.
See also Architecture Decision Records.
Roadmap and Demonstration¶
Roadmap¶
| Phase | Goal | Infrastructure | Exit criteria |
|---|---|---|---|
| MVP / demo | demonstrate a working system and core scenarios | VPS + Docker Compose | demo flow, backup, basic observability |
| Pilot | load real data and collect feedback | VPS + regular backup procedures; migrate to external S3 as media grows | import of a real collection, list of UX/data issues |
| First customers | ensure predictable operations | separate DB, external S3-compatible object storage, monitoring | paying organization, recovery procedure, support |
| Growth | prepare scaling and regional contours | separate DB, external S3-compatible object storage, separate workers (import), read optimization, regional strategy | tenant count growth, SLA expectations, regional tenant distribution |
Screenshots and Demo¶
Global map¶
Plant management¶
Places and boundaries management¶
AI-assisted plant import¶
What this project demonstrates¶
This project demonstrates my ability to:
- work at the intersection of system analysis, backend design, implementation, and deployment.
- take an idea through to a running system with multiple availability zones and a scaling roadmap
- strategically plan implementation of a small-to-medium-scale SaaS
- safely and in a controlled way apply LLM code generation for feature implementation



