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