diff --git a/Architecture.md b/Architecture.md new file mode 100644 index 0000000..dd74fb9 --- /dev/null +++ b/Architecture.md @@ -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. + \ No newline at end of file