πŸ—οΈ Building a Clean Architecture API with Go, Ore, and SQLite β€” DeepSeek Blog | Neura Market
    Neura MarketNeura Market/DeepSeek
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsBlogVideosGuidesCoursesCommunityTrendingGenerate
    DeepSeekBlogπŸ—οΈ Building a Clean Architecture API with Go, Ore, and SQLite
    Back to Blog
    πŸ—οΈ Building a Clean Architecture API with Go, Ore, and SQLite
    go

    πŸ—οΈ Building a Clean Architecture API with Go, Ore, and SQLite

    Firas M. Darwish March 7, 2026
    0 views

    So you've been writing Go for a bit. Your main.go is growing. You've got a database call next to an...

    --- title: πŸ—οΈ Building a Clean Architecture API with Go, Ore, and SQLite published: true description: tags: go, ore, di, golang, architecture cover_image: https://lilury.com/statics/article-cover.png # Use a ratio of 100:42 for best results. published_at: 2026-03-07 17:10 +0000 --- So you've been writing Go for a bit. Your `main.go` is growing. You've got a database call next to an HTTP handler next to a business rule, and somewhere in the back of your head a little voice keeps whispering *"this is going to be a nightmare to test."* That voice is right. And today we're going to silence it. 🀫 We're going to build a **Book Library REST API** from scratch using **Clean Architecture** - a layered design that keeps your business logic completely isolated from databases, HTTP frameworks, and anything else that changes for the wrong reasons. To wire it all together, we'll use **[Ore](https://ore.lilury.com)**, a lightweight dependency injection container for Go. And for persistence, we'll use **SQLite** via the `modernc.org/sqlite` driver (pure Go, no CGo required - your CI pipeline will thank you πŸ™). By the end of this guide you'll have: - πŸ“¦ A proper 4-layer Clean Architecture project structure - πŸ—„οΈ A real SQLite database with schema migration on startup - πŸ’‰ Ore managing all dependency wiring, lifetimes, and startup validation - πŸ”Œ Graceful shutdown that closes the DB connection cleanly Let's go. --- ## πŸ—ΊοΈ The Big Picture Before writing a single line of code, let's understand *why* Clean Architecture matters. The core rule is simple: **dependencies only point inward.** The domain layer (your business rules) knows nothing about HTTP or SQLite. The application layer (your use cases) knows the domain but not the database driver. Infrastructure (your SQLite repo) knows the domain interface it implements, but the domain doesn't know SQLite exists. ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Presentation (HTTP) β”‚ ← knows application β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Application (Use Cases) β”‚ ← knows domain β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Domain (Entities + Interfaces) β”‚ ← knows nothing β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Infrastructure (SQLite, etc.) β”‚ ← knows domain interfaces β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↑ Dependencies point inward ↑ ``` What makes this possible in Go is **interfaces**. The domain defines `BookRepository` as an interface. The infrastructure provides the concrete SQLite implementation. The application layer depends only on the interface. Ore acts as the **composition root** - the one place that says "okay, when someone needs a `BookRepository`, give them the SQLite one." Here's the project layout we'll build: ``` booklib/ β”œβ”€β”€ main.go β”œβ”€β”€ go.mod β”œβ”€β”€ domain/ β”‚ └── book.go # Entity + repository interface β”œβ”€β”€ application/ β”‚ └── book_service.go # Use cases β”œβ”€β”€ infrastructure/ β”‚ └── sqlite_repo.go # SQLite repository implementation β”œβ”€β”€ presentation/ β”‚ └── book_handler.go # HTTP handlers + route registration └── di/ └── container.go # 🎯 The composition root - all Ore wiring lives here ``` Notice `di/container.go` is the only file that imports all layers at once. Every other package stays in its lane. --- ## βš™οΈ Step 1: Initialize the Project ```bash mkdir booklib && cd booklib go mod init booklib # Ore for dependency injection go get -u github.com/firasdarwish/ore # Pure-Go SQLite driver - no CGo, no drama go get modernc.org/sqlite ``` > πŸ’‘ **Why `modernc.org/sqlite` over `mattn/go-sqlite3`?** The `mattn` driver requires CGo, which complicates cross-compilation and Docker builds. `modernc.org/sqlite` is a pure Go port - it compiles and runs anywhere Go does. --- ## 🧠 Step 2: The Domain Layer - The Heart of the App The domain layer is sacred. It contains your business entities and the *contracts* (interfaces) that describe what the app needs from the outside world. **No imports from other layers. No Ore. No `database/sql`. Nothing.** ```go // domain/book.go package domain import "errors" // Book is our core entity. It belongs to the domain, not to any database table. type Book struct { ID int64 Title string Author string } // These are domain errors - they describe business-level failures, // not HTTP status codes or SQL error codes. var ( ErrBookNotFound = errors.New("book not found") ErrInvalidBook = errors.New("book title and author are required") ) // BookRepository is the contract the domain needs from persistence. // It doesn't know or care that SQLite is on the other side. // Tomorrow it could be Postgres. The domain layer would never know. type BookRepository interface { Save(book Book) (Book, error) FindByID(id int64) (Book, error) FindAll() ([]Book, error) } ``` This is the most important file in the project - and it has zero external dependencies. That's the point. Your business rules don't need SQLite to exist. They don't need `net/http` to exist. They just need a `BookRepository` that works. --- ## πŸ”§ Step 3: The Application Layer - Where Use Cases Live The application layer orchestrates your use cases. It takes user intentions ("create a book"), applies domain rules (validate input), delegates to the repository (persist it), and returns results. It knows the domain package. It does **not** know SQLite, HTTP, or anything infrastructure-related. ```go // application/book_service.go package application import ( "booklib/domain" "context" "github.com/firasdarwish/ore" ) // BookService contains all the use cases for the book feature. type BookService struct { repo domain.BookRepository } // NewBookService is Ore's initializer for this service. // Notice it asks Ore for domain.BookRepository - an interface. // It has no idea that SQLite is behind it. 🀷 func NewBookService(ctx context.Context) (*BookService, context.Context) { repo, ctx := ore.Get[domain.BookRepository](ctx) return &BookService{repo: repo}, ctx } // CreateBook validates input and persists a new book. func (s *BookService) CreateBook(title, author string) (domain.Book, error) { if title == "" || author == "" { return domain.Book{}, domain.ErrInvalidBook } return s.repo.Save(domain.Book{Title: title, Author: author}) } // GetBook retrieves a book by its ID. func (s *BookService) GetBook(id int64) (domain.Book, error) { return s.repo.FindByID(id) } // ListBooks returns all books in the library. func (s *BookService) ListBooks() ([]domain.Book, error) { return s.repo.FindAll() } ``` The `NewBookService` initializer is doing something subtle but powerful: `ore.Get[domain.BookRepository](ctx)` asks Ore to resolve the `BookRepository` interface from the dependency graph. At this point in the code, we have absolutely no idea what the concrete type is. The DI container (`di/container.go`) will decide that later. --- ## πŸ—„οΈ Step 4: The Infrastructure Layer - SQLite Gets Its Hands Dirty Now for the fun part. The infrastructure layer is where we actually talk to SQLite. This package knows it's implementing `domain.BookRepository`. It knows about `database/sql`. It knows about SQL queries. The domain and application layers remain blissfully unaware of all this. ```go // infrastructure/sqlite_repo.go package infrastructure import ( "booklib/domain" "context" "database/sql" "errors" "log" "github.com/firasdarwish/ore" _ "modernc.org/sqlite" // registers the "sqlite" driver ) // DB is a thin wrapper around *sql.DB so Ore can manage its lifetime // as a distinct type in the container. type DB struct { *sql.DB } // NewDB opens the SQLite connection and runs schema migrations. // Registered as a Singleton - one DB pool for the entire app lifetime. func NewDB(ctx context.Context) (*DB, context.Context) { sqlDB, err := sql.Open("sqlite", "./books.db") if err != nil { log.Fatalf("❌ failed to open SQLite: %v", err) } // Tune the connection pool for SQLite's single-writer model sqlDB.SetMaxOpenConns(1) sqlDB.SetMaxIdleConns(1) if err := sqlDB.Ping(); err != nil { log.Fatalf("❌ SQLite ping failed: %v", err) } // Run schema migration - create the table if it doesn't exist _, err = sqlDB.Exec(` CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT NOT NULL ) `) if err != nil { log.Fatalf("❌ failed to run migrations: %v", err) } log.Println("βœ… SQLite connected and schema ready") return &DB{sqlDB}, ctx } // Shutdown closes the database connection. // Ore will call this at application exit via GetResolvedSingletons[Shutdowner](). func (db *DB) Shutdown() { log.Println("πŸ”Œ Closing SQLite connection...") if err := db.Close(); err != nil { log.Printf("⚠️ error closing DB: %v", err) } } // SQLiteBookRepository is the concrete implementation of domain.BookRepository. type SQLiteBookRepository struct { db *DB } // NewSQLiteBookRepository is Ore's initializer for the repository. // It resolves *DB from the container - Ore ensures it's the same singleton instance. func NewSQLiteBookRepository(ctx context.Context) (*SQLiteBookRepository, context.Context) { db, ctx := ore.Get[*DB](ctx) return &SQLiteBookRepository{db: db}, ctx } func (r *SQLiteBookRepository) Save(book domain.Book) (domain.Book, error) { result, err := r.db.Exec( "INSERT INTO books (title, author) VALUES (?, ?)", book.Title, book.Author, ) if err != nil { return domain.Book{}, err } id, err := result.LastInsertId() if err != nil { return domain.Book{}, err } book.ID = id return book, nil } func (r *SQLiteBookRepository) FindByID(id int64) (domain.Book, error) { row := r.db.QueryRow("SELECT id, title, author FROM books WHERE id = ?", id) var book domain.Book if err := row.Scan(&book.ID, &book.Title, &book.Author); err != nil { if errors.Is(err, sql.ErrNoRows) { return domain.Book{}, domain.ErrBookNotFound } return domain.Book{}, err } return book, nil } func (r *SQLiteBookRepository) FindAll() ([]domain.Book, error) { rows, err := r.db.Query("SELECT id, title, author FROM books ORDER BY id ASC") if err != nil { return nil, err } defer rows.Close() var books []domain.Book for rows.Next() { var book domain.Book if err := rows.Scan(&book.ID, &book.Title, &book.Author); err != nil { return nil, err } books = append(books, book) } return books, rows.Err() } ``` A few design decisions worth calling out: The `DB` wrapper type exists so that Ore can track `*DB` as a distinct named type in the container. Without the wrapper, we'd be registering `*sql.DB` - which is fine, but less descriptive and harder to extend later (e.g. if you wanted to add health check methods). `SetMaxOpenConns(1)` is important for SQLite. It's a file-based, single-writer database - opening many concurrent connections is counterproductive and can cause locking errors. One connection, properly managed, is the right call. The `Shutdown()` method is our exit hook. Ore will find this automatically when we call `ore.GetResolvedSingletons[Shutdowner]()` in `main.go`. --- ## 🌐 Step 5: The Presentation Layer - HTTP Talks to the World The presentation layer handles the HTTP surface of the app. It speaks JSON in, JSON out. It knows the application layer (to call use cases) but nothing about SQLite, domain errors beyond what it needs to map, or any infrastructure concern. ```go // presentation/book_handler.go package presentation import ( "booklib/application" "booklib/domain" "context" "encoding/json" "errors" "net/http" "strconv" "github.com/firasdarwish/ore" ) // BookHandler holds all the HTTP handler methods for books. type BookHandler struct { service *application.BookService } // NewBookHandler is Ore's initializer - it resolves *BookService from the container. func NewBookHandler(ctx context.Context) (*BookHandler, context.Context) { svc, ctx := ore.Get[*application.BookService](ctx) return &BookHandler{service: svc}, ctx } func (h *BookHandler) CreateBook(w http.ResponseWriter, r *http.Request) { var input struct { Title string `json:"title"` Author string `json:"author"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { http.Error(w, "invalid JSON body", http.StatusBadRequest) return } book, err := h.service.CreateBook(input.Title, input.Author) if err != nil { if errors.Is(err, domain.ErrInvalidBook) { http.Error(w, err.Error(), http.StatusBadRequest) return } http.Error(w, "something went wrong", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(book) } func (h *BookHandler) GetBook(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "invalid id", http.StatusBadRequest) return } book, err := h.service.GetBook(id) if err != nil { if errors.Is(err, domain.ErrBookNotFound) { http.Error(w, "book not found", http.StatusNotFound) return } http.Error(w, "something went wrong", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(book) } func (h *BookHandler) ListBooks(w http.ResponseWriter, r *http.Request) { books, err := h.service.ListBooks() if err != nil { http.Error(w, "something went wrong", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(books) } // RegisterRoutes wires each route to the handler. // The handler itself is resolved fresh per request via r.Context() - // this means BookHandler and BookService are Scoped to each HTTP request. func RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("POST /books", func(w http.ResponseWriter, r *http.Request) { handler, _ := ore.Get[*BookHandler](r.Context()) handler.CreateBook(w, r) }) mux.HandleFunc("GET /books/{id}", func(w http.ResponseWriter, r *http.Request) { handler, _ := ore.Get[*BookHandler](r.Context()) handler.GetBook(w, r) }) mux.HandleFunc("GET /books", func(w http.ResponseWriter, r *http.Request) { handler, _ := ore.Get[*BookHandler](r.Context()) handler.ListBooks(w, r) }) } ``` The route closures each call `ore.Get[*BookHandler](r.Context())`. This is the scoping magic at work - Ore uses `r.Context()` as the scope boundary. Every concurrent request gets its own `BookHandler` and its own `BookService`, completely isolated. The shared singleton `*DB` underneath is reused across all of them. --- ## πŸ’‰ Step 6: The DI Container - The Composition Root This is the most important wiring file in the whole project. `di/container.go` is the **only** place that imports all layers simultaneously. It's where you make the concrete choices: "use SQLite, not Postgres", "use this service, not a mock." ```go // di/container.go package di import ( "booklib/application" "booklib/domain" "booklib/infrastructure" "booklib/presentation" "github.com/firasdarwish/ore" ) // Register wires the entire application dependency graph into Ore. // This is the composition root - the one place that sees all layers. func Register() { // πŸ—„οΈ Register the SQLite DB as a Singleton. // One *DB instance for the whole application lifetime. // NewDB opens the connection and runs migrations on first resolution. ore.RegisterFunc[*infrastructure.DB]( ore.Singleton, infrastructure.NewDB, ) // πŸ“¦ Register the SQLite repository as a Singleton. // It depends on *DB, which Ore resolves above. ore.RegisterFunc[*infrastructure.SQLiteBookRepository]( ore.Singleton, infrastructure.NewSQLiteBookRepository, ) // πŸ”— Alias: whenever someone asks for domain.BookRepository (the interface), // Ore hands them *infrastructure.SQLiteBookRepository (the implementation). // This is dependency inversion - the application layer never knows SQLite exists. ore.RegisterAlias[domain.BookRepository, *infrastructure.SQLiteBookRepository]() // ⚑ Register BookService as Scoped - a fresh instance per request context. // It depends on domain.BookRepository, resolved via the alias above. ore.RegisterFunc[*application.BookService]( ore.Scoped, application.NewBookService, ) // 🌐 Register BookHandler as Scoped - a fresh instance per request context. // It depends on *application.BookService. ore.RegisterFunc[*presentation.BookHandler]( ore.Scoped, presentation.NewBookHandler, ) // πŸ”’ Seal the container - no new registrations allowed after this point. // Any attempt to register after Seal() will panic immediately, // preventing accidental runtime mutation of the dependency graph. ore.Seal() // βœ… Validate the full dependency graph at startup. // Ore will catch: // - Missing dependencies // - Circular dependencies // - Lifetime misalignments (e.g. a Singleton depending on a Scoped) // Better to crash loudly on startup than silently misbehave at 3am. πŸ”₯ ore.Validate() // ⚑ Disable per-call validation now that we've verified everything. // Ore validates on every Get() call by default - useful in development, // but unnecessary overhead once Validate() has blessed the graph. ore.DisableValidation = true } ``` The `RegisterAlias` line is the crux of Clean Architecture in code form. `BookService` calls `ore.Get[domain.BookRepository](ctx)` - asking for an interface. The alias tells Ore: "that interface? Give them `*SQLiteBookRepository`." If tomorrow you want to swap to Postgres, you change two lines in this file and nothing else in the project changes. --- ## πŸš€ Step 7: main.go - Startup, Routes, and Goodbye ```go // main.go package main import ( "booklib/di" "booklib/presentation" "context" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/firasdarwish/ore" ) // Shutdowner is our graceful-shutdown interface. // Any singleton that implements Shutdown() will be discovered automatically by Ore. // We define it here in main - Ore doesn't care where it's defined, // only that the type matches at runtime. type Shutdowner interface { Shutdown() } func main() { log.Println("πŸ“š BookLib starting up...") // Wire up everything - this is the only line in main that cares about DI di.Register() // Set up routes mux := http.NewServeMux() presentation.RegisterRoutes(mux) server := &http.Server{ Addr: ":8080", Handler: mux, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, } // Start serving in a goroutine so we can listen for shutdown signals below go func() { log.Println("🌍 Server listening on http://localhost:8080") if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("❌ server crashed: %v", err) } }() // Block until we receive SIGINT or SIGTERM (Ctrl+C or `kill`) quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("πŸ‘‹ Shutdown signal received - draining requests...") // Give in-flight requests 5 seconds to finish ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Printf("⚠️ HTTP shutdown error: %v", err) } // πŸ”‘ Graceful service cleanup via Ore. // GetResolvedSingletons finds every resolved singleton that implements Shutdowner. // Crucially, Ore returns them in REVERSE dependency order - // so the DB is closed AFTER any services that depend on it. // No manual enumeration. No forgetting a service. Ore handles it. πŸ’ͺ log.Println("🧹 Cleaning up singletons...") shutdownables := ore.GetResolvedSingletons[Shutdowner]() for _, s := range shutdownables { s.Shutdown() } log.Println("βœ… Clean exit. Goodbye!") } ``` The shutdown sequence is worth reading twice. We don't call `db.Close()` explicitly anywhere in `main.go`. We don't maintain a list of "things to clean up." We just ask Ore: "give me every resolved singleton that can shut itself down, in dependency order." Ore knows the graph - it knows `*DB` was resolved as a dependency of `*SQLiteBookRepository`, so `*DB.Shutdown()` runs after `SQLiteBookRepository` is done. As you add more singletons (caches, queue publishers, audit log flushers), they just need to implement `Shutdown()` and Ore includes them automatically. --- ## πŸ§ͺ Let's Take It for a Spin ```bash go run main.go # πŸ“š BookLib starting up... # βœ… SQLite connected and schema ready # 🌍 Server listening on http://localhost:8080 ``` In another terminal: ```bash # βž• Create a few books curl -s -X POST http://localhost:8080/books \ -H "Content-Type: application/json" \ -d '{"title":"The Go Programming Language","author":"Donovan & Kernighan"}' | jq # { # "ID": 1, # "Title": "The Go Programming Language", # "Author": "Donovan & Kernighan" # } curl -s -X POST http://localhost:8080/books \ -H "Content-Type: application/json" \ -d '{"title":"Clean Architecture","author":"Robert C. Martin"}' | jq # πŸ“š List all books curl -s http://localhost:8080/books | jq # πŸ” Get a specific book curl -s http://localhost:8080/books/1 | jq # ❌ Try a bad request curl -s -X POST http://localhost:8080/books \ -H "Content-Type: application/json" \ -d '{"title":""}' | jq # book title and author are required # Press Ctrl+C: # πŸ‘‹ Shutdown signal received - draining requests... # 🧹 Cleaning up singletons... # πŸ”Œ Closing SQLite connection... # βœ… Clean exit. Goodbye! ``` Restart the server and the books are still there - they're in `books.db`, a real SQLite file on disk. --- ## πŸ”¬ How Ore Sees the Dependency Graph Here's what happens under the hood when a `GET /books/1` request comes in: ``` r.Context() β”‚ β–Ό ore.Get[*BookHandler](r.Context()) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ [Scoped] *BookHandler β”‚ ← fresh per request β”‚ depends on ↓ β”‚ β”‚ [Scoped] *BookService β”‚ ← fresh per request β”‚ depends on ↓ β”‚ β”‚ [Alias] domain.BookRepository β”‚ ← resolves to ↓ β”‚ [Singleton] *SQLiteBookRepo β”‚ ← shared across all requests β”‚ depends on ↓ β”‚ β”‚ [Singleton] *DB β”‚ ← one connection pool, always β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` The two singletons at the bottom are created once, ever. The scoped services are created fresh for each request and garbage collected when the request context ends. Ore manages all of this from the registrations in `di/container.go`. You never call `new()` manually for any of these types. --- ## πŸ”„ The Payoff: Swapping SQLite for Postgres Remember when we said this architecture makes swapping databases a two-line change? Here's the proof. Add a `PostgresBookRepository` to infrastructure, then in `di/container.go`: ```go // Before (SQLite): ore.RegisterFunc[*infrastructure.SQLiteBookRepository](ore.Singleton, infrastructure.NewSQLiteBookRepository) ore.RegisterAlias[domain.BookRepository, *infrastructure.SQLiteBookRepository]() // After (Postgres): ore.RegisterFunc[*infrastructure.PostgresBookRepository](ore.Singleton, infrastructure.NewPostgresBookRepository) ore.RegisterAlias[domain.BookRepository, *infrastructure.PostgresBookRepository]() ``` `BookService`, `BookHandler`, your tests, your domain rules - none of them change. Not a single line. That's the whole point. 🎯 --- ## πŸ§ͺ Testing with Isolated Containers One of Ore's killer features for testing is `ore.NewContainer()`. Each test gets its own isolated dependency graph - no shared state, no flaky tests, no "it passes locally but fails in CI" mysteries. ```go func TestCreateBook(t *testing.T) { // 🧱 Build a fresh, isolated container just for this test container := ore.NewContainer() // Register a mock repo instead of the real SQLite one ore.RegisterFuncToContainer[domain.BookRepository](container, ore.Scoped, func(ctx context.Context) (domain.BookRepository, context.Context) { return &MockBookRepository{}, ctx }, ) // Wire up BookService - same initializer as production ore.RegisterFuncToContainer[*application.BookService](container, ore.Scoped, application.NewBookService, ) ctx := context.Background() svc, _ := ore.GetFromContainer[*application.BookService](container, ctx) book, err := svc.CreateBook("Clean Code", "Robert Martin") assert.NoError(t, err) assert.Equal(t, "Clean Code", book.Title) assert.Equal(t, "Robert Martin", book.Author) } ``` The mock repo satisfies `domain.BookRepository`. `BookService` doesn't know or care. The test runs fast with zero disk I/O. πŸš€ --- ## πŸ’‘ Ore Patterns You'll Use Again and Again By building this app, you've touched the core toolkit: `RegisterFunc` with `ore.Singleton` - for anything expensive to create and safe to share: DB connections, HTTP clients, config structs. `RegisterFunc` with `ore.Scoped` - for anything that should be isolated per request: services, handlers, transaction managers. `RegisterAlias` - the dependency inversion trick. Register a concrete, alias an interface to it. Every caller gets the interface; Ore handles the mapping. `ore.Seal()` + `ore.Validate()` - your safety net. Call them both at startup. Catch misconfigured graphs before the first request, not during a production incident. `ore.GetResolvedSingletons[T]()` - the graceful shutdown pattern. Define a cleanup interface, let your services implement it, and Ore gives you a sorted list at exit time. No maintenance required as the app grows. --- ## 🏁 Wrapping Up Here's what we built and why it matters: **The domain layer** has zero external dependencies. Your business rules could be tested in a brand new Go project with just the standard library. That's power. **The application layer** depends only on domain interfaces. It doesn't know if it's talking to SQLite, Postgres, a mock, or a remote API. It just does its job. **The infrastructure layer** does the dirty work - SQL queries, connection management, schema migrations - and implements exactly the interface the domain asked for. **The presentation layer** translates HTTP to application calls and back. If you add gRPC next month, it's just a new presentation file. **The DI container** is the only place that sees the whole picture. Changing the database is two lines. Swapping to a mock in tests is five lines. The rest of the project never notices. 😌 Clean Architecture and Ore are a natural match. Ore makes the composition root explicit, validated, and lifecycle-aware. Clean Architecture gives every piece of your app a clear home and clear rules for how it can talk to its neighbors. Now go build something real. πŸš€ --- *Full Ore documentation: [ore.lilury.com](https://ore.lilury.com) - source on [GitHub](https://github.com/firasdarwish/ore)*

    Tags

    goorediarchitecture

    Comments

    More Blog

    View all
    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠ai

    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠

    Hi everyone! πŸ‘‹ I’m Tara, a Senior Software Engineer and Consultant. Over the years, I've jumped...

    T
    tworrell
    Local AI Will Save Us All (The Math Says So, Trust Me)ai

    Local AI Will Save Us All (The Math Says So, Trust Me)

    Every few weeks a take goes viral in tech circles making the case for ditching cloud AI and running...

    S
    Sebastian SchΓΌrmann
    Lost in the AI Hype, I Started Smallai

    Lost in the AI Hype, I Started Small

    And it helped me get back into tech without drowning TL;DR at the end Coming back to...

    R
    Rohini Gaonkar
    Building a Replay-Tested Interactive Brokers Client in Gogo

    Building a Replay-Tested Interactive Brokers Client in Go

    I wanted an IBKR library that felt like Go and had testing I could trust. So I wrote one.

    T
    Thomas Marcelis
    Playwright in Pictures: Fully Parallel Modeplaywright

    Playwright in Pictures: Fully Parallel Mode

    Playwright’s fullyParallel mode is often treated as a simple performance switch. In practice, it...

    V
    Vitaliy Potapov
    Designing a CLI for Both Humans and Agentscli

    Designing a CLI for Both Humans and Agents

    Learn how Alpic designed its CLI for both human developers and AI agents β€” covering tradeoffs like polling, context windows, interactivity, and statelessness.

    J
    Julien Vallini

    Stay up to date

    Get the latest DeepSeek prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for DeepSeek and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    Β© 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.