Add Architecture
116
Architecture.md
Normal file
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.
|
||||||
|
|
||||||
Reference in New Issue
Block a user