From d1047e2b7258fc402dc5a09061660bdab6a7610a Mon Sep 17 00:00:00 2001
From: softwarespot <softwarespotapps@gmail.com>
Date: Mon, 14 Oct 2024 22:42:05 +0300
Subject: [PATCH] Initial commit

---
 .env.example                                  |   3 +
 .github/workflows/go.yml                      |  23 ++
 .gitignore                                    |  27 +++
 Dockerfile                                    |  23 ++
 LICENSE                                       |  20 ++
 Makefile                                      |  22 ++
 README.md                                     | 219 ++++++++++++++++++
 cmd/execute.go                                |  28 +++
 cmd/help.go                                   |  18 ++
 cmd/helpers.go                                |  40 ++++
 cmd/server.go                                 |  71 ++++++
 cmd/version.go                                |  29 +++
 go.mod                                        |   3 +
 internal/date-time/helpers.go                 |  15 ++
 internal/env/get.go                           |  14 ++
 internal/env/load-test/.env.invalid           |   5 +
 internal/env/load-test/.env.valid             |   4 +
 internal/env/load-test/get_test.go            |  42 ++++
 internal/env/load-test/helpers_test.go        |   9 +
 internal/env/load-test/load_test.go           |  57 +++++
 internal/env/load.go                          |  51 ++++
 internal/holidays/county_code.go              |  16 ++
 internal/holidays/handler_dk.go               |  28 +++
 internal/holidays/handler_fi.go               |  35 +++
 internal/holidays/handler_no.go               |  29 +++
 internal/holidays/handler_se.go               |  36 +++
 internal/holidays/helpers.go                  |  35 +++
 internal/holidays/holiday.go                  |  15 ++
 internal/holidays/manager.go                  |  34 +++
 internal/logging/helpers.go                   |  33 +++
 internal/logging/level.go                     |  14 ++
 internal/logging/logger.go                    |  10 +
 internal/logging/memory.go                    |  25 ++
 internal/logging/stdout_logger.go             |  45 ++++
 internal/service/args.go                      |  16 ++
 internal/service/error.go                     |  34 +++
 .../service/middleware/concurrent_requests.go |  27 +++
 internal/service/middleware/metrics.go        |  27 +++
 internal/service/middleware/panic_recovery.go |  39 ++++
 internal/service/response_writer.go           |  77 ++++++
 internal/service/server.go                    | 137 +++++++++++
 internal/version/version.go                   |  15 ++
 main.go                                       |  21 ++
 test-helpers/assert.go                        |  30 +++
 44 files changed, 1501 insertions(+)
 create mode 100644 .env.example
 create mode 100644 .github/workflows/go.yml
 create mode 100644 .gitignore
 create mode 100644 Dockerfile
 create mode 100644 LICENSE
 create mode 100644 Makefile
 create mode 100644 README.md
 create mode 100644 cmd/execute.go
 create mode 100644 cmd/help.go
 create mode 100644 cmd/helpers.go
 create mode 100644 cmd/server.go
 create mode 100644 cmd/version.go
 create mode 100644 go.mod
 create mode 100644 internal/date-time/helpers.go
 create mode 100644 internal/env/get.go
 create mode 100644 internal/env/load-test/.env.invalid
 create mode 100644 internal/env/load-test/.env.valid
 create mode 100644 internal/env/load-test/get_test.go
 create mode 100644 internal/env/load-test/helpers_test.go
 create mode 100644 internal/env/load-test/load_test.go
 create mode 100644 internal/env/load.go
 create mode 100644 internal/holidays/county_code.go
 create mode 100644 internal/holidays/handler_dk.go
 create mode 100644 internal/holidays/handler_fi.go
 create mode 100644 internal/holidays/handler_no.go
 create mode 100644 internal/holidays/handler_se.go
 create mode 100644 internal/holidays/helpers.go
 create mode 100644 internal/holidays/holiday.go
 create mode 100644 internal/holidays/manager.go
 create mode 100644 internal/logging/helpers.go
 create mode 100644 internal/logging/level.go
 create mode 100644 internal/logging/logger.go
 create mode 100644 internal/logging/memory.go
 create mode 100644 internal/logging/stdout_logger.go
 create mode 100644 internal/service/args.go
 create mode 100644 internal/service/error.go
 create mode 100644 internal/service/middleware/concurrent_requests.go
 create mode 100644 internal/service/middleware/metrics.go
 create mode 100644 internal/service/middleware/panic_recovery.go
 create mode 100644 internal/service/response_writer.go
 create mode 100644 internal/service/server.go
 create mode 100644 internal/version/version.go
 create mode 100644 main.go
 create mode 100644 test-helpers/assert.go

diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..b6f9131
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,3 @@
+ENV=production
+SERVER_PORT=<SERVER_PORT>
+SERVER_MAX_CONCURRENCY=<SERVER_MAX_CONCURRENCY>
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 0000000..a1e466b
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,23 @@
+# This workflow will build a golang project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
+
+name: Go
+on: [push, pull_request]
+
+jobs:
+    test:
+        strategy:
+            matrix:
+                go-version: [1.23.x]
+
+        runs-on: ubuntu-latest
+        steps:
+            - uses: actions/checkout@v4
+
+            - name: Set up Go
+              uses: actions/setup-go@v5
+              with:
+                  go-version: ${{ matrix.go-version }}
+
+            - name: Test
+              run: go test -cover -v ./...
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aeb4e9b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+# Taken from URL: https://github.com/github/gitignore/blob/main/Go.gitignore
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+go.work.sum
+
+# env file
+.env
+.env.production
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..42c3e17
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,23 @@
+# See URL: https://hub.docker.com/_/golang
+# Use the Go image to build the binary only
+FROM golang:1.23.0 AS builder
+ENV CGO_ENABLED=0
+ENV GOOS=linux
+WORKDIR /go/src/public-holidays/
+COPY . .
+
+# Overwrite the development .env with the .env.production
+COPY .env.production .env
+
+RUN make
+
+# See URL: https://hub.docker.com/_/alpine
+# Use this image (~50MB) to run the "public-holidays", as the Go image contains too much bloat,
+# which isn't needed for running the application in production and the image which can be uploaded
+# to a public/private Docker register is then small
+FROM alpine:3.20.3
+
+COPY --from=builder /go/src/public-holidays/public-holidays ./
+COPY --from=builder /go/src/public-holidays/.env ./
+EXPOSE 10000
+CMD ["./public-holidays"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1b2ef48
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2024 SoftwareSpot Apps
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c12a4b6
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,22 @@
+# If the "VERSION" environment variable is not set, then use this value instead
+VERSION?=1.0.0
+TIME=$(shell date +%FT%T%z)
+GOVERSION=$(shell go version | awk '{print $$3}' | sed s/go//)
+
+LDFLAGS=-ldflags "\
+	-X github.com/softwarespot/public-holidays/internal/version.Version=${VERSION} \
+	-X github.com/softwarespot/public-holidays/internal/version.Time=${TIME} \
+	-X github.com/softwarespot/public-holidays/internal/version.User=${USER} \
+	-X github.com/softwarespot/public-holidays/internal/version.GoVersion=${GOVERSION} \
+	-s \
+	-w \
+"
+
+build:
+	@echo building to public-holidays
+	@go build $(LDFLAGS) -o public-holidays
+
+test:
+	go test -cover -v ./...
+
+.PHONY: build test
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a9ae828
--- /dev/null
+++ b/README.md
@@ -0,0 +1,219 @@
+# Public holidays
+
+![Go Tests](https://github.com/softwarespot/public-holidays/actions/workflows/go.yml/badge.svg)
+
+This project showcases my proficiency in Go by creating a clear and readable
+public holidays API. The primary focus is on writing clean, maintainable code
+that effectively demonstrates the logic behind retrieving public holidays for a
+country's ISO 3166-1 alpha-2 code.
+
+## GET /holidays/v1/{country}/{year}
+
+This endpoint retrieves a list of public holidays for a specified country and
+year. By replacing `{country}` with the two-letter ISO 3166-1 alpha-2 code of
+the desired country and `{year}` with the target year, users can access detailed
+information about each holiday, including dates, local names and English names.
+This API is useful for applications that need to display or utilize holiday data
+for various purposes, such as scheduling or planning.
+
+## Hosted by [render.com](https://render.com/)
+
+This API is available at https://public-holidays.onrender.com.
+
+**IMPORTANT**
+The instance might be down due to inactivity, therefore, wait about 50s for the instance to be started again.
+
+### Example request
+
+```bash
+curl -s "https://public-holidays.onrender.com/holidays/v1/FI/2024" | jq
+
+# or locally
+
+curl -s "http://localhost:10000/holidays/v1/FI/2024" | jq
+```
+
+### Response format
+
+The response will be in JSON format and includes an array of holiday objects
+with the following details:
+
+-   **date**: The date of the holiday.
+-   **name**: The name of the holiday in the local language.
+-   **englishName**: The name of the holiday in English.
+
+```json
+[
+    {
+        "date": "2024-01-01",
+        "name": "Uudenvuodenpäivä",
+        "englishName": "New Year's Day"
+    },
+    {
+        "date": "2024-01-06",
+        "name": "Loppiainen",
+        "englishName": "Epiphany"
+    },
+
+    ...
+]
+```
+
+## Countries
+
+Here is a list of the supported countries.
+
+| Country    | ISO 3166-1 alpha-2 | Wikipedia Link                                                                         |
+| ---------- | ------------------ | -------------------------------------------------------------------------------------- |
+| 🇩🇰 Denmark | DK                 | [Public Holidays in Denmark](https://en.wikipedia.org/wiki/Public_holidays_in_Denmark) |
+| 🇫🇮 Finland | FI                 | [Public Holidays in Finland](https://en.wikipedia.org/wiki/Public_holidays_in_Finland) |
+| 🇳🇴 Norway  | NO                 | [Public Holidays in Norway](https://en.wikipedia.org/wiki/Public_holidays_in_Norway)   |
+| 🇸🇪 Sweden  | SE                 | [Public Holidays in Sweden](https://en.wikipedia.org/wiki/Public_holidays_in_Sweden)   |
+
+## Prerequisites
+
+-   Go 1.23.0 or above
+-   make (if you want to use the `Makefile` provided)
+-   Docker
+
+## Dependencies
+
+**IMPORTANT:** No 3rd party dependencies are used.
+
+I could easily use [Cobra](https://github.com/spf13/cobra) (and usually I do,
+because it allows me to write powerful CLIs), but I felt it was too much for
+such a tiny project. I only ever use dependencies when it's say an adapter for
+an external service e.g. Redis, MySQL or Prometheus.
+
+## Setup
+
+1. Create and edit the `.env` (used when developing locally) and `.env.production` (used when deploying to production)
+
+```bash
+cp .env.example .env
+cp .env.example .env.production
+```
+
+## Run not using Docker
+
+```bash
+go run .
+```
+
+or when using `make`
+
+```bash
+make
+
+./public-holidays
+```
+
+### Version
+
+Display the version of the application and exit.
+
+```bash
+make
+
+# As text
+./public-holidays --version
+
+# As JSON
+./public-holidays --json --version
+```
+
+### Help
+
+Display the help text and exit.
+
+```bash
+make
+
+./public-holidays --help
+```
+
+## Run using Docker
+
+1. Build the Docker image with the tag `public-holidays`.
+
+```bash
+docker build -t public-holidays .
+```
+
+2. Run the Docker image.
+
+```bash
+# Port number should the same as defined in ".env.production"
+docker run -p "10000:10000" --rm public-holidays
+```
+
+### Version
+
+Display the version of the application and exit.
+
+```bash
+# As text
+docker run --rm public-holidays --version
+
+# As JSON
+docker run --rm public-holidays --json --version
+```
+
+### Help
+
+Display the help text and exit.
+
+```bash
+docker run --rm public-holidays --help
+```
+
+## Tests
+
+Tests are written as [Table-Driven Tests](https://go.dev/wiki/TableDrivenTests).
+
+```bash
+go test -cover -v ./...
+```
+
+or when using `make`
+
+```bash
+make test
+```
+
+### Coverage
+
+```bash
+go test -cover -v github.com/softwarespot/public-holidays/internal/
+```
+
+### Linting
+
+Docker
+
+```bash
+docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run -v --tests=false --disable-all -E durationcheck,errorlint,exhaustive,gocritic,gosimple,ineffassign,misspell,predeclared,revive,staticcheck,unparam,unused,whitespace --max-issues-per-linter=10000 --max-same-issues=10000
+```
+
+Local
+
+```bash
+golangci-lint run --tests=false --disable-all -E durationcheck,errorlint,exhaustive,gocritic,gosimple,ineffassign,misspell,predeclared,revive,staticcheck,unparam,unused,whitespace --max-issues-per-linter=10000 --max-same-issues=10000
+```
+
+## Additional information
+
+This section documents any additional information which might be deemed
+important for the reviewer.
+
+### Decisions made
+
+-   Despite using 1.23.0+ and the `slices` pkg being available, I have opted not
+    to use it, and instead went for how I've been writing Go code before the
+    `slices` pkg existed. Although for production code, I have started to use it
+    where applicable.
+-   Loosely used https://jsonapi.org/.
+
+### License
+
+The code has been licensed under the [MIT](https://opensource.org/license/mit) license.
diff --git a/cmd/execute.go b/cmd/execute.go
new file mode 100644
index 0000000..c1d2b52
--- /dev/null
+++ b/cmd/execute.go
@@ -0,0 +1,28 @@
+package cmd
+
+import (
+	"flag"
+
+	"github.com/softwarespot/public-holidays/internal/logging"
+)
+
+func Execute(_ []string, logger logging.Logger) error {
+	var showHelp bool
+	flagBoolVarP(&showHelp, "help", "h", false, "Display the help text and exit")
+
+	var showVersion bool
+	flagBoolVarP(&showVersion, "version", "v", false, "Display the version of the application and exit")
+
+	var asJSON bool
+	flagBoolVarP(&asJSON, "json", "j", false, "Output the result as JSON")
+	flag.Parse()
+
+	if showHelp {
+		cmdHelp()
+		return nil
+	}
+	if showVersion {
+		return cmdVersion(asJSON)
+	}
+	return cmdServer(logger)
+}
diff --git a/cmd/help.go b/cmd/help.go
new file mode 100644
index 0000000..2a0c25f
--- /dev/null
+++ b/cmd/help.go
@@ -0,0 +1,18 @@
+package cmd
+
+import "fmt"
+
+func cmdHelp() {
+	helpText := `Usage: ./public-holidays [OPTIONS]
+
+Start the public holidays API.
+
+Options:
+  -h, --help      Show this help text and exit.
+  -v, --version   Display the version of the application and exit.
+  -j, --json      Output the version as JSON.
+
+Examples:
+  ./public-holidays`
+	fmt.Println(helpText)
+}
diff --git a/cmd/helpers.go b/cmd/helpers.go
new file mode 100644
index 0000000..1e278a1
--- /dev/null
+++ b/cmd/helpers.go
@@ -0,0 +1,40 @@
+package cmd
+
+import (
+	"flag"
+	"fmt"
+	"math"
+	"strconv"
+)
+
+// The naming has been taken from "pflag" i.e. used in "cobra". URL: https://pkg.go.dev/github.com/spf13/pflag#BoolVarP
+func flagBoolVarP(p *bool, name, shorthand string, value bool, usage string) {
+	flag.BoolVar(p, name, value, usage)
+	flag.BoolVar(p, shorthand, value, usage)
+}
+
+func parseMaxConcurrency(s string) (int32, error) {
+	v, err := strconv.Atoi(s)
+	if err != nil {
+		return 0, fmt.Errorf("invalid max concurrency of %q. Expected an integer of 32 bits", s)
+	}
+	if v < math.MinInt32 || v > math.MaxInt32 {
+		return 0, fmt.Errorf("invalid max concurrency of %q. Expected an integer of 32 bits", s)
+	}
+	return int32(v), nil
+}
+
+func parseYear(s string) (int, error) {
+	if len(s) != 4 {
+		return 0, fmt.Errorf("invalid year of %q. Expected a valid 4 digit year", s)
+	}
+
+	year, err := strconv.Atoi(s)
+	if err != nil {
+		return 0, fmt.Errorf("invalid year of %q. Expected a valid 4 digit year", s)
+	}
+	if year <= 0 {
+		return 0, fmt.Errorf("invalid year of %q. Expected a valid 4 digit year", s)
+	}
+	return year, nil
+}
diff --git a/cmd/server.go b/cmd/server.go
new file mode 100644
index 0000000..a3ffc87
--- /dev/null
+++ b/cmd/server.go
@@ -0,0 +1,71 @@
+package cmd
+
+import (
+	"context"
+	"net/http"
+	"os"
+	"os/signal"
+	"syscall"
+
+	"github.com/softwarespot/public-holidays/internal/env"
+	"github.com/softwarespot/public-holidays/internal/holidays"
+	"github.com/softwarespot/public-holidays/internal/logging"
+	"github.com/softwarespot/public-holidays/internal/service"
+	"github.com/softwarespot/public-holidays/internal/service/middleware"
+)
+
+func cmdServer(logger logging.Logger) error {
+	port := env.Get("SERVER_PORT", "10000")
+	maxConcurrency, err := parseMaxConcurrency(env.Get("SERVER_MAX_CONCURRENCY", "500"))
+	if err != nil {
+		return err
+	}
+
+	rw := service.NewResponseWriter(logger)
+	s := service.NewServer(":"+port, logger)
+	s.Use(
+		middleware.NewPanicRecovery(func(w http.ResponseWriter, r *http.Request, err error) { rw.ErrorAsJSON(w, r, err) }, logger),
+		middleware.NewMetrics(logger),
+		middleware.NewConcurrentRequests(int32(maxConcurrency), logger),
+	)
+
+	hm := holidays.NewManager()
+	s.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) error {
+		http.Redirect(w, r, "https://github.com/softwarespot/public-holidays", http.StatusMovedPermanently)
+		return nil
+	})
+
+	routeHolidays := "GET /holidays/v1/{countryCode}/{year}"
+	s.HandleFunc(routeHolidays, func(w http.ResponseWriter, r *http.Request) error {
+		headers := w.Header()
+		headers.Set("Access-Control-Allow-Origin", "*")
+		headers.Set("Access-Control-Allow-Methods", "GET")
+
+		// Disable the API from being indexed by search engines e.g. Google.
+		// See URL: https://developers.google.com/search/reference/robots_meta_tag
+		headers.Set("X-Robots-Tag", "none")
+
+		code, err := holidays.NewCountryCode(r.PathValue("countryCode"))
+		if err != nil {
+			return service.NewError(err, http.StatusBadRequest)
+		}
+
+		year, err := parseYear(r.PathValue("year"))
+		if err != nil {
+			return service.NewError(err, http.StatusBadRequest)
+		}
+
+		res, err := hm.Get(code, year)
+		if err != nil {
+			return service.NewError(err, http.StatusBadRequest)
+		}
+		return rw.WriteAsJSON(w, r, res)
+	})
+	s.HandleErrorFunc(routeHolidays, func(w http.ResponseWriter, r *http.Request, err error) {
+		rw.ErrorAsJSON(w, r, err)
+	})
+
+	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
+	defer cancel()
+	return s.ListenAndServe(ctx)
+}
diff --git a/cmd/version.go b/cmd/version.go
new file mode 100644
index 0000000..ff6b83d
--- /dev/null
+++ b/cmd/version.go
@@ -0,0 +1,29 @@
+package cmd
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+
+	"github.com/softwarespot/public-holidays/internal/version"
+)
+
+func cmdVersion(asJSON bool) error {
+	if !asJSON {
+		fmt.Printf("Version:\t%s\n", version.Version)
+		fmt.Printf("Build Time:\t%s\n", version.Time)
+		fmt.Printf("Build User:\t%s\n", version.User)
+		fmt.Printf("Go Version:\t%s\n", version.GoVersion)
+		return nil
+	}
+
+	// This could be a struct, but it would be a temporary struct in that case.
+	// Therefore, a map is honestly enough for this
+	out := map[string]string{
+		"version":   version.Version,
+		"buildTime": version.Time,
+		"buildUser": version.User,
+		"goVersion": version.GoVersion,
+	}
+	return json.NewEncoder(os.Stdout).Encode(out)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..38e24d1
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/softwarespot/public-holidays
+
+go 1.23.0
diff --git a/internal/date-time/helpers.go b/internal/date-time/helpers.go
new file mode 100644
index 0000000..cfb3cdc
--- /dev/null
+++ b/internal/date-time/helpers.go
@@ -0,0 +1,15 @@
+package datetime
+
+import "time"
+
+const (
+	// ISO 8601 date format i.e. YYYY-MM-DD
+	layoutDateOnly = "2006-01-02"
+)
+
+func ToDateString(t time.Time) string {
+	if t.IsZero() {
+		return "0000-00-00"
+	}
+	return t.Format(layoutDateOnly)
+}
diff --git a/internal/env/get.go b/internal/env/get.go
new file mode 100644
index 0000000..6f814e5
--- /dev/null
+++ b/internal/env/get.go
@@ -0,0 +1,14 @@
+package env
+
+import "os"
+
+// Get retrieves the value of the environment variable named
+// by the key. If the variable is present in the environment the
+// value (which may be empty) is returned.
+// Otherwise the returned value will be the fallback value
+func Get(key, fallback string) string {
+	if v, ok := os.LookupEnv(key); ok {
+		return v
+	}
+	return fallback
+}
diff --git a/internal/env/load-test/.env.invalid b/internal/env/load-test/.env.invalid
new file mode 100644
index 0000000..ddd31bc
--- /dev/null
+++ b/internal/env/load-test/.env.invalid
@@ -0,0 +1,5 @@
+TEST_1=variable 1
+# TEST_2
+
+Test2
+Test3=1234
diff --git a/internal/env/load-test/.env.valid b/internal/env/load-test/.env.valid
new file mode 100644
index 0000000..96d5b6c
--- /dev/null
+++ b/internal/env/load-test/.env.valid
@@ -0,0 +1,4 @@
+TEST_1=variable 1
+# TEST_2=variable 2
+
+Test3=1234
diff --git a/internal/env/load-test/get_test.go b/internal/env/load-test/get_test.go
new file mode 100644
index 0000000..1fb78ee
--- /dev/null
+++ b/internal/env/load-test/get_test.go
@@ -0,0 +1,42 @@
+package loadtest
+
+import (
+	"os"
+	"testing"
+
+	"github.com/softwarespot/public-holidays/internal/env"
+	testhelpers "github.com/softwarespot/public-holidays/test-helpers"
+)
+
+func Test_Get(t *testing.T) {
+	tests := []struct {
+		name     string
+		key      string
+		fallback string
+		want     string
+	}{
+		{
+			name:     "exists",
+			key:      "TEST_1",
+			fallback: "fallback 1",
+			want:     "variable 1",
+		},
+		{
+			name:     "doesn't exist",
+			key:      "TEST_3",
+			fallback: "fallback 3",
+			want:     "fallback 3",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			defer envCleanup()
+
+			err := env.Load(os.DirFS("."), ".env.valid")
+			testhelpers.AssertNoError(t, err)
+
+			got := env.Get(tt.key, tt.fallback)
+			testhelpers.AssertEqual(t, got, tt.want)
+		})
+	}
+}
diff --git a/internal/env/load-test/helpers_test.go b/internal/env/load-test/helpers_test.go
new file mode 100644
index 0000000..8d2ea9c
--- /dev/null
+++ b/internal/env/load-test/helpers_test.go
@@ -0,0 +1,9 @@
+package loadtest
+
+import "os"
+
+func envCleanup() {
+	os.Unsetenv("TEST_1")
+	os.Unsetenv("TEST_2")
+	os.Unsetenv("Test3")
+}
diff --git a/internal/env/load-test/load_test.go b/internal/env/load-test/load_test.go
new file mode 100644
index 0000000..b85ed0b
--- /dev/null
+++ b/internal/env/load-test/load_test.go
@@ -0,0 +1,57 @@
+package loadtest
+
+import (
+	"os"
+	"testing"
+
+	"github.com/softwarespot/public-holidays/internal/env"
+	testhelpers "github.com/softwarespot/public-holidays/test-helpers"
+)
+
+func Test_Load(t *testing.T) {
+	tests := []struct {
+		name    string
+		envFile string
+		want    map[string]string
+		wantErr bool
+	}{
+		{
+			name:    "missing .env file",
+			envFile: ".env.missing",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "invalid .env file",
+			envFile: ".env.invalid",
+			want: map[string]string{
+				"TEST_1": "variable 1",
+			},
+			wantErr: true,
+		},
+		{
+			name:    "load .env file",
+			envFile: ".env.valid",
+			want: map[string]string{
+				"TEST_1": "variable 1",
+				"TEST_2": "",
+				"Test3":  "1234",
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			defer envCleanup()
+
+			err := env.Load(os.DirFS("."), tt.envFile)
+			testhelpers.AssertEqual(t, (err != nil), tt.wantErr)
+
+			for key, want := range tt.want {
+				v, ok := os.LookupEnv(key)
+				testhelpers.AssertEqual(t, v, want)
+				testhelpers.AssertEqual(t, ok, want != "")
+			}
+		})
+	}
+}
diff --git a/internal/env/load.go b/internal/env/load.go
new file mode 100644
index 0000000..a208214
--- /dev/null
+++ b/internal/env/load.go
@@ -0,0 +1,51 @@
+package env
+
+import (
+	"bufio"
+	"fmt"
+	"io/fs"
+	"os"
+	"strings"
+)
+
+// Load reads environment variables from a specified file and sets them in the current process's environment.
+//
+// Example usage:
+//
+//	err := Load(os.DirFS("."), ".env")
+//	if err != nil {
+//	    log.Fatalf("error loading .env: %+v", err)
+//	}
+func Load(fs fs.FS, path string) error {
+	f, err := fs.Open(path)
+	if err != nil {
+		return fmt.Errorf("unable to open the env file: %w", err)
+	}
+	defer f.Close()
+
+	lineNo := 0
+	scanner := bufio.NewScanner(f)
+	for scanner.Scan() {
+		lineNo++
+		line := strings.TrimSpace(scanner.Text())
+		if line == "" || strings.HasPrefix(line, "#") {
+			continue
+		}
+
+		kv := strings.SplitN(line, "=", 2)
+		if len(kv) != 2 {
+			return fmt.Errorf("invalid env line format at line %d: %s", lineNo, line)
+		}
+
+		key := strings.TrimSpace(kv[0])
+		value := strings.TrimSpace(kv[1])
+		if err := os.Setenv(key, value); err != nil {
+			return fmt.Errorf("unable to set env variable at line %d, key: %s, value: %s: %w", lineNo, key, value, err)
+		}
+	}
+
+	if err = scanner.Err(); err != nil {
+		return fmt.Errorf("unable to scan the env file: %w", err)
+	}
+	return nil
+}
diff --git a/internal/holidays/county_code.go b/internal/holidays/county_code.go
new file mode 100644
index 0000000..dc14be3
--- /dev/null
+++ b/internal/holidays/county_code.go
@@ -0,0 +1,16 @@
+package holidays
+
+import (
+	"fmt"
+	"strings"
+)
+
+type CountryCode string
+
+func NewCountryCode(code string) (CountryCode, error) {
+	code = strings.ToUpper(code)
+	if len(code) != 2 {
+		return "", fmt.Errorf("invalid county code of %q. Expected an ISO 3166-1 alpha-2 country code", code)
+	}
+	return CountryCode(code), nil
+}
diff --git a/internal/holidays/handler_dk.go b/internal/holidays/handler_dk.go
new file mode 100644
index 0000000..49e0302
--- /dev/null
+++ b/internal/holidays/handler_dk.go
@@ -0,0 +1,28 @@
+package holidays
+
+import (
+	"strconv"
+
+	datetime "github.com/softwarespot/public-holidays/internal/date-time"
+)
+
+func dk(year int) ([]Holiday, error) {
+	strYear := strconv.Itoa(year)
+
+	easter := calculateCatholicEaster(year)
+
+	// Specification: https://en.wikipedia.org/wiki/Public_holidays_in_Denmark
+	return []Holiday{
+		newHoliday(strYear+"-01-01", "Nytårsdag", "New Year's Day"),
+		newHoliday(strYear+"-04-18", "Skærtorsdag", "Maundy Thursday"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, -2)), "Langfredag", "Good Friday"),
+		newHoliday(datetime.ToDateString(easter), "Påskedag", "Easter Sunday"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 1)), "Anden påskedag", "Easter Monday"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 40)), "Kristi himmelfartsdag", "Ascension Day"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 49)), "Pinsedag", "Pentecost Sunday"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 50)), "Anden pinsedag", "Whit Monday"),
+		newHoliday(strYear+"-06-05", "Grundlovsdag", "Constitution Day"),
+		newHoliday(strYear+"-12-25", "Juledag", "Christmas Day"),
+		newHoliday(strYear+"-12-26", "Anden juledag", "St. Stephen's Day"),
+	}, nil
+}
diff --git a/internal/holidays/handler_fi.go b/internal/holidays/handler_fi.go
new file mode 100644
index 0000000..441446f
--- /dev/null
+++ b/internal/holidays/handler_fi.go
@@ -0,0 +1,35 @@
+package holidays
+
+import (
+	"strconv"
+	"time"
+
+	datetime "github.com/softwarespot/public-holidays/internal/date-time"
+)
+
+func fi(year int) ([]Holiday, error) {
+	strYear := strconv.Itoa(year)
+
+	easter := calculateCatholicEaster(year)
+	midsummer := locateDay(year, time.June, 20, time.Saturday)
+	allSaints := locateDay(year, time.October, 31, time.Saturday)
+
+	// Specificiation: https://en.wikipedia.org/wiki/Public_holidays_in_Finland
+	return []Holiday{
+		newHoliday(strYear+"-01-01", "Uudenvuodenpäivä", "New Year's Day"),
+		newHoliday(strYear+"-01-06", "Loppiainen", "Epiphany"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, -2)), "Pitkäperjantai", "Good Friday"),
+		newHoliday(datetime.ToDateString(easter), "Pääsiäispäivä", "Easter Sunday"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 1)), "2. pääsiäispäivä", "Easter Monday"),
+		newHoliday(strYear+"-05-01", "Vappu", "May Day"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 39)), "Helatorstai", "Ascension Day"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 49)), "Helluntaipäivä", "Pentecost Sunday"),
+		newHoliday(datetime.ToDateString(midsummer.AddDate(0, 0, -1)), "Juhannusaatto", "Midsummer Eve"),
+		newHoliday(datetime.ToDateString(midsummer), "Juhannuspäivä", "Midsummer Day"),
+		newHoliday(datetime.ToDateString(allSaints), "Pyhäinpäivä", "All Saints' Day"),
+		newHoliday(strYear+"-12-06", "Itsenäisyyspäivä", "Independence Day"),
+		newHoliday(strYear+"-12-24", "Jouluaatto", "Christmas Eve"),
+		newHoliday(strYear+"-12-25", "Joulupäivä", "Christmas Day"),
+		newHoliday(strYear+"-12-26", "2. joulupäivä", "St. Stephen's Day"),
+	}, nil
+}
diff --git a/internal/holidays/handler_no.go b/internal/holidays/handler_no.go
new file mode 100644
index 0000000..25a7493
--- /dev/null
+++ b/internal/holidays/handler_no.go
@@ -0,0 +1,29 @@
+package holidays
+
+import (
+	"strconv"
+
+	datetime "github.com/softwarespot/public-holidays/internal/date-time"
+)
+
+func no(year int) ([]Holiday, error) {
+	strYear := strconv.Itoa(year)
+
+	easter := calculateCatholicEaster(year)
+
+	// Specificiation: https://en.wikipedia.org/wiki/Public_holidays_in_Norway
+	return []Holiday{
+		newHoliday(strYear+"-01-01", "Første nyttårsdag", "New Year's Day"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, -3)), "Skjærtorsdag", "Maundy Thursday"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, -2)), "Långfredagen", "Good Friday"),
+		newHoliday(datetime.ToDateString(easter), "Første påskedag", "Easter Sunday"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 1)), "Andre påskedag", "Easter Monday"),
+		newHoliday(strYear+"-05-01", "Første mai", "May Day"),
+		newHoliday(strYear+"-05-17", "Grunnlovsdagen", "Constitution Day"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 39)), "Kristi himmelfartsdag", "Ascension Day"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 49)), "Første pinsedag", "Pentecost Sunday"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 50)), "Andre pinsedag", "Whit Monday"),
+		newHoliday(strYear+"-12-25", "Første juledag", "Christmas Day"),
+		newHoliday(strYear+"-12-26", "Andre juledag", "St. Stephen's Day"),
+	}, nil
+}
diff --git a/internal/holidays/handler_se.go b/internal/holidays/handler_se.go
new file mode 100644
index 0000000..d1086de
--- /dev/null
+++ b/internal/holidays/handler_se.go
@@ -0,0 +1,36 @@
+package holidays
+
+import (
+	"strconv"
+	"time"
+
+	datetime "github.com/softwarespot/public-holidays/internal/date-time"
+)
+
+func se(year int) ([]Holiday, error) {
+	strYear := strconv.Itoa(year)
+
+	easter := calculateCatholicEaster(year)
+	midsummer := locateDay(year, time.June, 20, time.Saturday)
+	allSaints := locateDay(year, time.October, 31, time.Saturday)
+
+	// Specificiation: https://en.wikipedia.org/wiki/Public_holidays_in_Sweden
+	return []Holiday{
+		newHoliday(strYear+"-01-01", "Nyårsdagen", "New Year's Day"),
+		newHoliday(strYear+"-01-06", "Trettondedag jul", "Epiphany"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, -2)), "Långfredagen", "Good Friday"),
+		newHoliday(datetime.ToDateString(easter), "Påskdagen", "Easter Sunday"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 1)), "Annandag påsk", "Easter Monday"),
+		newHoliday(strYear+"-05-01", "Första Maj", "May Day"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 39)), "Kristi himmelsfärds dag", "Ascension Day"),
+		newHoliday(datetime.ToDateString(easter.AddDate(0, 0, 49)), "Pingstdagen", "Pentecost Sunday"),
+		newHoliday(strYear+"-06-06", "Sveriges nationaldag", "National Day of Sweden"),
+		newHoliday(datetime.ToDateString(midsummer.AddDate(0, 0, -1)), "Midsommarafton", "Midsummer Eve"),
+		newHoliday(datetime.ToDateString(midsummer), "Midsommardagen", "Midsummer Day"),
+		newHoliday(datetime.ToDateString(allSaints), "Alla helgons dag", "All Saints' Day"),
+		newHoliday(strYear+"-12-24", "Julafton", "Christmas Eve"),
+		newHoliday(strYear+"-12-25", "Juldagen", "Christmas Day"),
+		newHoliday(strYear+"-12-26", "Nyårsafton", "St. Stephen's Day"),
+		newHoliday(strYear+"-12-31", "Annandag jul", "New Year's Eve"),
+	}, nil
+}
diff --git a/internal/holidays/helpers.go b/internal/holidays/helpers.go
new file mode 100644
index 0000000..305f653
--- /dev/null
+++ b/internal/holidays/helpers.go
@@ -0,0 +1,35 @@
+package holidays
+
+import "time"
+
+type handlerFunc func(year int) ([]Holiday, error)
+
+// Taken from URL: http://stackoverflow.com/questions/2510383/how-can-i-calculate-what-date-good-friday-falls-on-given-a-year
+func calculateCatholicEaster(y int) time.Time {
+	g := y % 19
+	c := y / 100
+	h := (c - c/4 - (8*c+13)/25 + 19*g + 15) % 30
+	i := h - h/28*(1-h/28*(29/(h+1))*((21-g)/11))
+
+	day := i - (y+y/4+i+2-c+c/4)%7 + 28
+	month := 3
+
+	if day > 31 {
+		month++
+		day -= 31
+	}
+	return time.Date(y, time.Month(month), day, 0, 0, 0, 0, time.Local)
+}
+
+func locateDay(year int, month time.Month, day int, targetWeekday time.Weekday) time.Time {
+	start := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
+	if start.Weekday() == targetWeekday {
+		return start
+	}
+
+	daysUntilTarget := int(targetWeekday - start.Weekday())
+	if daysUntilTarget < 0 {
+		daysUntilTarget += 7
+	}
+	return start.AddDate(0, 0, daysUntilTarget)
+}
diff --git a/internal/holidays/holiday.go b/internal/holidays/holiday.go
new file mode 100644
index 0000000..63c694c
--- /dev/null
+++ b/internal/holidays/holiday.go
@@ -0,0 +1,15 @@
+package holidays
+
+type Holiday struct {
+	Date        string `json:"date"`
+	Name        string `json:"name"`
+	EnglishName string `json:"englishName"`
+}
+
+func newHoliday(date, name, englishName string) Holiday {
+	return Holiday{
+		Date:        date,
+		Name:        name,
+		EnglishName: englishName,
+	}
+}
diff --git a/internal/holidays/manager.go b/internal/holidays/manager.go
new file mode 100644
index 0000000..972b54f
--- /dev/null
+++ b/internal/holidays/manager.go
@@ -0,0 +1,34 @@
+package holidays
+
+import "fmt"
+
+type Manager struct {
+	// No mutex is needed, as this is written to only on initialization
+	handlers map[CountryCode]handlerFunc
+}
+
+func NewManager() *Manager {
+	m := &Manager{
+		handlers: map[CountryCode]handlerFunc{},
+	}
+
+	// Registered country code handlers
+	m.register("DK", dk)
+	m.register("FI", fi)
+	m.register("NO", no)
+	m.register("SE", se)
+
+	return m
+}
+
+func (m *Manager) register(code CountryCode, handler handlerFunc) {
+	m.handlers[code] = handler
+}
+
+func (m *Manager) Get(code CountryCode, year int) ([]Holiday, error) {
+	handlerFn, ok := m.handlers[code]
+	if !ok {
+		return nil, fmt.Errorf("unsupported country code of %q", code)
+	}
+	return handlerFn(year)
+}
diff --git a/internal/logging/helpers.go b/internal/logging/helpers.go
new file mode 100644
index 0000000..b6cc95d
--- /dev/null
+++ b/internal/logging/helpers.go
@@ -0,0 +1,33 @@
+package logging
+
+import (
+	"os"
+	"runtime/debug"
+	"time"
+)
+
+func createEntry(msg string, level Level, args ...any) map[string]any {
+	entry := map[string]any{}
+
+	if len(args) > 0 && len(args)%2 != 0 {
+		args = append(args, "%ARGS NOT DIVISIBLE BY 2%")
+	}
+	for i := 0; i < len(args); i += 2 {
+		key, ok := args[i].(string)
+		if !ok {
+			// Skip keys which are not a string
+			continue
+		}
+		entry[key] = args[i+1]
+	}
+
+	entry["@timestamp"] = time.Now().UTC().Format(time.RFC3339)
+	entry["@message"] = msg
+	entry["@level"] = level
+	entry["@env"] = os.Getenv("ENV")
+
+	if level.IsSevere() {
+		entry["@stack-trace"] = string(debug.Stack())
+	}
+	return entry
+}
diff --git a/internal/logging/level.go b/internal/logging/level.go
new file mode 100644
index 0000000..25b0c5f
--- /dev/null
+++ b/internal/logging/level.go
@@ -0,0 +1,14 @@
+package logging
+
+type Level string
+
+const (
+	LevelCritical Level = "critical"
+	LevelError    Level = "error"
+	LevelWarning  Level = "warning"
+	LevelNotice   Level = "notice"
+)
+
+func (l Level) IsSevere() bool {
+	return l == LevelCritical || l == LevelError || l == LevelWarning
+}
diff --git a/internal/logging/logger.go b/internal/logging/logger.go
new file mode 100644
index 0000000..8ff8978
--- /dev/null
+++ b/internal/logging/logger.go
@@ -0,0 +1,10 @@
+package logging
+
+// Logger is an interface, as it allows different logging adapters to be created e.g.
+// a file logger, a logger which sends to Slack etc.
+type Logger interface {
+	// NOTE: This MUST exit the process
+	Fatal(err error, code int, args ...any)
+	LogError(err error, level Level, args ...any)
+	Log(msg string, level Level, args ...any)
+}
diff --git a/internal/logging/memory.go b/internal/logging/memory.go
new file mode 100644
index 0000000..b23ec8c
--- /dev/null
+++ b/internal/logging/memory.go
@@ -0,0 +1,25 @@
+package logging
+
+import "runtime"
+
+// Memory logs memory statistics using the provided logger.
+// It retrieves the current memory allocation statistics and logs them
+// along with any additional arguments provided
+func Memory(logger Logger, msg string, logArgs []any) {
+	var m runtime.MemStats
+	runtime.ReadMemStats(&m)
+
+	logger.Log(
+		msg,
+		LevelNotice,
+		append(
+			[]any{
+				"alloc-mb", m.Alloc / 1024 / 1024,
+				"total-alloc-mb", m.TotalAlloc / 1024 / 1024,
+				"sys-mb", m.Sys / 1024 / 1024,
+				"num-gc", m.NumGC,
+			},
+			logArgs...,
+		)...,
+	)
+}
diff --git a/internal/logging/stdout_logger.go b/internal/logging/stdout_logger.go
new file mode 100644
index 0000000..bf643cf
--- /dev/null
+++ b/internal/logging/stdout_logger.go
@@ -0,0 +1,45 @@
+package logging
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"sync"
+)
+
+// Ensure interface compatibility
+var _ Logger = &StdoutLogger{}
+
+// StdoutLogger is a logger that writes log entries to standard output in JSON format
+type StdoutLogger struct {
+	enc *json.Encoder
+	mu  sync.Mutex
+}
+
+// NewStdoutLogger creates a new instance of StdoutLogger
+func NewStdoutLogger() *StdoutLogger {
+	return &StdoutLogger{
+		enc: json.NewEncoder(os.Stdout),
+	}
+}
+
+// Fatal logs a critical error message and exits the application with the specified exit code
+func (sl *StdoutLogger) Fatal(err error, code int, args ...any) {
+	sl.LogError(err, LevelCritical, args...)
+	os.Exit(code)
+}
+
+// LogError logs an error message with the specified log level
+func (sl *StdoutLogger) LogError(err error, level Level, args ...any) {
+	sl.Log(err.Error(), level, args...)
+}
+
+// Log writes a log entry with a message, log level, and optional additional arguments, which must be divisible by 2
+func (sl *StdoutLogger) Log(msg string, level Level, args ...any) {
+	sl.mu.Lock()
+	defer sl.mu.Unlock()
+
+	if err := sl.enc.Encode(createEntry(msg, level, args...)); err != nil {
+		fmt.Fprintf(os.Stderr, "skipped logging entry due to error: %+v. Message: '%s', Level: %v\n", err, msg, level)
+	}
+}
diff --git a/internal/service/args.go b/internal/service/args.go
new file mode 100644
index 0000000..970a5a6
--- /dev/null
+++ b/internal/service/args.go
@@ -0,0 +1,16 @@
+package service
+
+import "net/http"
+
+func Args(r *http.Request, args ...any) []any {
+	return append(
+		[]any{
+			"url", r.URL.RequestURI(),
+			"http-method", r.Method,
+			"ip", r.RemoteAddr,
+			"real-ip", r.Header.Get("X-Real-IP"),
+			"forwarded-ip", r.Header.Get("X-Forwarded-For"),
+		},
+		args...,
+	)
+}
diff --git a/internal/service/error.go b/internal/service/error.go
new file mode 100644
index 0000000..86a6db8
--- /dev/null
+++ b/internal/service/error.go
@@ -0,0 +1,34 @@
+package service
+
+type Error interface {
+	error
+	Unwrap() error
+	Status() int
+}
+
+type statusError struct {
+	err        error
+	statusCode int
+}
+
+func NewError(err error, statusCode int) error {
+	if err == nil {
+		return nil
+	}
+	return statusError{
+		err:        err,
+		statusCode: statusCode,
+	}
+}
+
+func (e statusError) Error() string {
+	return e.err.Error()
+}
+
+func (e statusError) Unwrap() error {
+	return e.err
+}
+
+func (e statusError) Status() int {
+	return e.statusCode
+}
diff --git a/internal/service/middleware/concurrent_requests.go b/internal/service/middleware/concurrent_requests.go
new file mode 100644
index 0000000..1741dbc
--- /dev/null
+++ b/internal/service/middleware/concurrent_requests.go
@@ -0,0 +1,27 @@
+package middleware
+
+import (
+	"errors"
+	"net/http"
+	"sync/atomic"
+
+	"github.com/softwarespot/public-holidays/internal/logging"
+	"github.com/softwarespot/public-holidays/internal/service"
+)
+
+func NewConcurrentRequests(maxConcurrency int32, logger logging.Logger) service.MiddlewareFunc {
+	logger.Log("loaded concurrent requests middleware", logging.LevelNotice,
+		"max-concurrency", maxConcurrency,
+	)
+
+	var concurrency atomic.Int32
+	return func(next service.Handler) service.Handler {
+		return service.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
+			defer concurrency.Add(-1)
+			if concurrency.Add(1) > maxConcurrency {
+				return service.NewError(errors.New(http.StatusText(http.StatusServiceUnavailable)), http.StatusServiceUnavailable)
+			}
+			return next.ServeHTTP(w, r)
+		})
+	}
+}
diff --git a/internal/service/middleware/metrics.go b/internal/service/middleware/metrics.go
new file mode 100644
index 0000000..8f396dd
--- /dev/null
+++ b/internal/service/middleware/metrics.go
@@ -0,0 +1,27 @@
+package middleware
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/softwarespot/public-holidays/internal/logging"
+	"github.com/softwarespot/public-holidays/internal/service"
+)
+
+func NewMetrics(logger logging.Logger) service.MiddlewareFunc {
+	logger.Log("loaded metrics middleware", logging.LevelNotice)
+
+	return func(next service.Handler) service.Handler {
+		return service.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
+			t0 := time.Now()
+			err := next.ServeHTTP(w, r)
+			logging.Memory(logger, "handled request",
+				service.Args(r,
+					"took", time.Since(t0).String(),
+					"error", err,
+				),
+			)
+			return err
+		})
+	}
+}
diff --git a/internal/service/middleware/panic_recovery.go b/internal/service/middleware/panic_recovery.go
new file mode 100644
index 0000000..5b2bad2
--- /dev/null
+++ b/internal/service/middleware/panic_recovery.go
@@ -0,0 +1,39 @@
+package middleware
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/softwarespot/public-holidays/internal/logging"
+	"github.com/softwarespot/public-holidays/internal/service"
+)
+
+type PanicRecoveryFunc func(http.ResponseWriter, *http.Request, error)
+
+func NewPanicRecovery(fn PanicRecoveryFunc, logger logging.Logger) service.MiddlewareFunc {
+	logger.Log("loaded panic recovery middleware", logging.LevelNotice)
+
+	return func(next service.Handler) service.Handler {
+		return service.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
+			defer panicRecovery(w, r, fn)
+			return next.ServeHTTP(w, r)
+		})
+	}
+}
+
+func panicRecovery(w http.ResponseWriter, r *http.Request, fn PanicRecoveryFunc) {
+	if rvr := recover(); rvr != nil {
+		var err error
+		switch e := rvr.(type) {
+		case error:
+			err = fmt.Errorf("recovered panic: %w", e)
+		default:
+			err = fmt.Errorf("%v", e)
+		}
+		if err == nil {
+			err = errors.New("unexpected nil error")
+		}
+		fn(w, r, err)
+	}
+}
diff --git a/internal/service/response_writer.go b/internal/service/response_writer.go
new file mode 100644
index 0000000..e805951
--- /dev/null
+++ b/internal/service/response_writer.go
@@ -0,0 +1,77 @@
+package service
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/softwarespot/public-holidays/internal/logging"
+)
+
+type ResponseWriter struct {
+	logger logging.Logger
+}
+
+func NewResponseWriter(logger logging.Logger) *ResponseWriter {
+	return &ResponseWriter{
+		logger: logger,
+	}
+}
+
+func (rw *ResponseWriter) WriteAsJSON(w http.ResponseWriter, _ *http.Request, res any) error {
+	// See URL: https://journal.petrausch.info/post/2020/06/golang-json-encoder-http-response-writer/,
+	// which explains why an error can be returned
+	w.Header().Set("Content-Type", "application/json")
+	if err := json.NewEncoder(w).Encode(res); err != nil {
+		return fmt.Errorf("unable to encode the HTTP application/json response: %w", err)
+	}
+	return nil
+}
+
+func (rw *ResponseWriter) ErrorAsJSON(w http.ResponseWriter, r *http.Request, err error, args ...any) {
+	errMsg, statusCode := getErrorStatus(err)
+	args = append([]any{"status-code", statusCode}, args...)
+	rw.logger.LogError(err, logging.LevelError, Args(r, args)...)
+
+	// Similar to "http.Error()"
+	h := w.Header()
+	h.Del("Content-Length")
+	h.Set("Content-Type", "application/json")
+	h.Set("X-Content-Type-Options", "nosniff")
+	w.WriteHeader(statusCode)
+
+	res := map[string]any{
+		"msg":  errMsg,
+		"code": statusCode,
+		"url":  r.URL.RequestURI(),
+	}
+	if err := json.NewEncoder(w).Encode(res); err != nil {
+		err = fmt.Errorf("unable to encode the HTTP error response: %w", err)
+		rw.logger.LogError(err, logging.LevelError, Args(r, args)...)
+	}
+}
+
+func (rw *ResponseWriter) Error(w http.ResponseWriter, r *http.Request, err error, args ...any) {
+	writeErrorAsText(w, r, err, rw.logger, args...)
+}
+
+func writeErrorAsText(w http.ResponseWriter, r *http.Request, err error, logger logging.Logger, args ...any) {
+	errMsg, statusCode := getErrorStatus(err)
+	args = append([]any{"status-code", statusCode}, args...)
+	logger.LogError(err, logging.LevelError, Args(r, args)...)
+	http.Error(w, errMsg, statusCode)
+}
+
+func getErrorStatus(err error) (string, int) {
+	var (
+		errMsg     = http.StatusText(http.StatusInternalServerError)
+		statusCode = http.StatusInternalServerError
+		e          Error
+	)
+	if errors.As(err, &e) {
+		errMsg = e.Unwrap().Error()
+		statusCode = e.Status()
+	}
+	return errMsg, statusCode
+}
diff --git a/internal/service/server.go b/internal/service/server.go
new file mode 100644
index 0000000..b62836b
--- /dev/null
+++ b/internal/service/server.go
@@ -0,0 +1,137 @@
+package service
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/softwarespot/public-holidays/internal/logging"
+)
+
+type Handler interface {
+	ServeHTTP(http.ResponseWriter, *http.Request) error
+}
+
+type HandlerFunc func(http.ResponseWriter, *http.Request) error
+
+func (h HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
+	return h(w, r)
+}
+
+type MiddlewareFunc func(Handler) Handler
+
+type Server struct {
+	logger               logging.Logger
+	server               *http.Server
+	mux                  *http.ServeMux
+	middlewares          []MiddlewareFunc
+	errHandlersByPattern map[string]func(w http.ResponseWriter, r *http.Request, err error)
+}
+
+func NewServer(addr string, logger logging.Logger) *Server {
+	mux := http.NewServeMux()
+	return &Server{
+		logger: logger,
+		server: &http.Server{
+			Addr:    addr,
+			Handler: mux,
+
+			// Fix potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server
+			ReadHeaderTimeout: 10 * time.Second,
+		},
+		mux:                  mux,
+		middlewares:          nil,
+		errHandlersByPattern: make(map[string]func(w http.ResponseWriter, r *http.Request, err error)),
+	}
+}
+
+// Use adds one or more middleware functions to the server.
+// Middleware functions are executed in the order they are added,
+// wrapping the handler functions registered with HandleFunc.
+// This allows for cross-cutting concerns such as logging, authentication,
+// and request timing to be handled consistently across all routes
+func (s *Server) Use(middleware ...MiddlewareFunc) {
+	s.middlewares = append(s.middlewares, middleware...)
+}
+
+// Handle registers the handler for the given pattern.
+// If the given pattern conflicts, with one that is already registered, Handle
+// panics
+func (s *Server) Handle(pattern string, handler Handler) {
+	s.HandleFunc(pattern, handler.ServeHTTP)
+}
+
+// HandleFunc registers the handler function for the given pattern.
+// If the given pattern conflicts, with one that is already registered, HandleFunc
+// panics
+func (s *Server) HandleFunc(pattern string, handler func(w http.ResponseWriter, r *http.Request) error) {
+	h := s.applyMiddlewareHandlers(HandlerFunc(handler))
+	s.mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
+		if err := h.ServeHTTP(w, r); err != nil {
+			if h, ok := s.errHandlersByPattern[pattern]; ok {
+				h(w, r, err)
+			} else {
+				writeErrorAsText(w, r, err, s.logger)
+			}
+		}
+	})
+}
+
+// HandleErrorFunc registers the error handler function for the given pattern.
+// If the given pattern conflicts, with one that is already registered, HandleErrorFunc
+// panics
+func (s *Server) HandleErrorFunc(pattern string, handler func(w http.ResponseWriter, r *http.Request, err error)) {
+	if handler == nil {
+		panic("nil error handler")
+	}
+	if _, ok := s.errHandlersByPattern[pattern]; ok {
+		panic("multiple error handler registrations for " + pattern)
+	}
+	s.errHandlersByPattern[pattern] = handler
+}
+
+func (s *Server) applyMiddlewareHandlers(handler Handler) Handler {
+	// Ensure the middleware handlers are executed in the same order they were registered i.e. FIFO
+	for i := len(s.middlewares) - 1; i >= 0; i-- {
+		mwh := s.middlewares[i]
+		handler = mwh(handler)
+	}
+	return handler
+}
+
+// ListenAndServe starts the HTTP server (in a separate go routine) and listens for incoming requests.
+// If an error occurs while starting the server, it will return that error.
+// The method also handles graceful shutdown when the provided context is done
+func (s *Server) ListenAndServe(ctx context.Context) error {
+	errCh := make(chan error, 1)
+	go func() {
+		// Ignore the error if nil or is a server closed error
+		if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
+			errCh <- fmt.Errorf("server unexpectedly closed: %w", err)
+		}
+	}()
+
+	s.logger.Log("started listening", logging.LevelNotice,
+		"address", s.server.Addr,
+	)
+
+	// Wait for either the context to be done or a non-closing error from the "ListenAndServe()" function
+	select {
+	case <-ctx.Done():
+	case err := <-errCh:
+		return err
+	}
+
+	s.logger.Log("start graceful server shutdown", logging.LevelNotice,
+		"address", s.server.Addr,
+		"context-error", ctx.Err(),
+	)
+	err := s.server.Shutdown(ctx)
+	s.logger.Log("stop graceful server shutdown", logging.LevelNotice,
+		"address", s.server.Addr,
+		"shutdown-error", err,
+	)
+	return err
+}
diff --git a/internal/version/version.go b/internal/version/version.go
new file mode 100644
index 0000000..9041340
--- /dev/null
+++ b/internal/version/version.go
@@ -0,0 +1,15 @@
+package version
+
+var (
+	// Version defines the version of the application
+	Version = "0.00"
+
+	// Time defines the timestamp at compilation
+	Time = "-"
+
+	// User defines the user who compiled the application
+	User = "-"
+
+	// GoVersion defines the Go version used at compilation
+	GoVersion = "0.00"
+)
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..06038d5
--- /dev/null
+++ b/main.go
@@ -0,0 +1,21 @@
+package main
+
+import (
+	"os"
+
+	"github.com/softwarespot/public-holidays/cmd"
+	"github.com/softwarespot/public-holidays/internal/env"
+	"github.com/softwarespot/public-holidays/internal/logging"
+)
+
+func main() {
+	logger := logging.NewStdoutLogger()
+	if err := env.Load(os.DirFS("."), ".env"); err != nil {
+		logger.Fatal(err, 1)
+	}
+
+	// Remove the executable name
+	if err := cmd.Execute(os.Args[1:], logger); err != nil {
+		logger.Fatal(err, 1)
+	}
+}
diff --git a/test-helpers/assert.go b/test-helpers/assert.go
new file mode 100644
index 0000000..664c636
--- /dev/null
+++ b/test-helpers/assert.go
@@ -0,0 +1,30 @@
+package testhelpers
+
+import (
+	"reflect"
+	"testing"
+)
+
+// AssertEqual checks if two values are equal. If they are not, it logs using t.Fatalf()
+func AssertEqual[T any](t testing.TB, got, correct T) {
+	t.Helper()
+	if !reflect.DeepEqual(got, correct) {
+		t.Fatalf("AssertEqual: expected values to be equal, got:\n%+v\ncorrect:\n%+v", got, correct)
+	}
+}
+
+// AssertError checks if an error is not nil. If it's nil, it logs using t.Fatalf()
+func AssertError(t testing.TB, err error) {
+	t.Helper()
+	if err == nil {
+		t.Fatalf("AssertError: expected an error, got nil")
+	}
+}
+
+// AssertNoError checks if an error is nil. If it's not nil, it logs using t.Fatalf()
+func AssertNoError(t testing.TB, err error) {
+	t.Helper()
+	if err != nil {
+		t.Fatalf("AssertNoError: expected no error, got %+v", err)
+	}
+}