Add Architecture

2026-03-21 07:26:15 +00:00
parent 0cccebf741
commit 5091eea00b

116
Architecture.md Normal file

@@ -0,0 +1,116 @@
# Architecture
## Overview
The FP&A Engine is a single-binary Go application. There are no external services, no message queues, and no separate migration tools. SQLite is embedded via a pure-Go driver, so the entire application ships as one file.
---
## Project Structure
```
FPandA-Engine/
├── main.go # Entry point: wiring, routing, graceful shutdown
├── Engine/
│ └── internal/
│ ├── database/ # DB connection, migration, and repository implementations
│ │ ├── connect.go
│ │ ├── migrate.go
│ │ ├── reference_repo.go
│ │ ├── budget_repo.go
│ │ └── actuals_repo.go
│ ├── handler/ # HTTP layer — decode request, call service, encode response
│ │ ├── reference.go
│ │ ├── budget.go
│ │ ├── actuals.go
│ │ └── variance.go
│ ├── model/ # Domain types (structs shared across layers)
│ │ ├── budget.go
│ │ ├── actuals.go
│ │ └── variance.go
│ └── service/ # Business logic
│ ├── budget_service.go
│ └── variance_service.go
├── tests/ # Go test files (uses stdlib testing, not Python)
├── go.mod
├── go.sum
└── .gitignore
```
---
## Layered Design
The application follows a clean three-layer architecture:
```
HTTP Request
Handler ← decodes JSON, validates input, calls service, encodes response
Service ← business logic: variance calculation, favourability, budget rules
Repository ← SQL queries against SQLite via database/sql
SQLite (fpa.db)
```
Each layer depends only on interfaces one level below it. Services do not know about HTTP; repositories do not know about business rules.
---
## Startup Sequence
`main.go` wires the application in this order:
1. Logger (`log/slog` with JSON output to stdout)
2. Database connection (`database.Connect`)
3. Auto-migration (`database.Migrate` — idempotent `CREATE TABLE IF NOT EXISTS`)
4. Reference repositories and handlers (departments, GL accounts — FK parents)
5. Budget and actuals repositories
6. Services wrapping those repositories
7. Handlers wrapping those services
8. Route registration on `http.NewServeMux()`
9. `http.Server` with explicit timeouts (read: 10s, write: 30s, idle: 120s)
10. Signal listener for graceful shutdown on `SIGINT`/`SIGTERM`
---
## Routing
Uses Go 1.22's built-in method+path routing pattern on `net/http.ServeMux`. No third-party router is needed.
```go
mux.HandleFunc("POST /api/v1/budgets", budgetH.Create)
mux.HandleFunc("PUT /api/v1/budgets/{id}", budgetH.Update)
```
---
## Database Schema
Three logical groups of tables:
**Reference data** (must exist before budgets/actuals — FK parents):
- `departments` — department codes and names
- `gl_accounts` — GL codes, descriptions, account type, and `favour_high` flag
**Transactional data**:
- `budgets` — amount by department + GL account + fiscal period + version
- `actuals` — upserted by period + department + GL account (idempotent)
Schema is applied automatically at startup. See `internal/database/migrate.go` for the full DDL.
---
## Why SQLite
SQLite is sufficient for FP&A data volumes (hundreds of departments, thousands of GL lines, 12 periods per year). Using a file-based embedded database means:
- Zero infrastructure to provision or maintain
- The database travels with the binary as a single `.db` file
- Backups are a file copy
- Tests use `:memory:` with no cleanup required
If the data volume grows to the point where SQLite becomes a bottleneck, the repository layer can be swapped for Postgres without touching the service or handler layers.