← Back to Cecep Azhar
Cecep Azhar #erp#golang#ai#module-development#architecture

Building Custom ERP Modules with Go and AI Integration

Cecep Saeful Azhar • 16 Oct 2026
Building Custom ERP Modules with Go and AI Integration
Off-the-shelf ERP modules rarely fit your business perfectly. Here's how to build custom ERP modules in Go with built-in AI capabilities — from architecture to deployment.

Why Build Custom ERP Modules

Standard ERP modules are designed for the average company. Your company isn't average.

You have unique workflows, specific regulations (especially in Indonesia — PPN, PPh23, perda lokal), and processes that don't fit any standard module. Instead of forcing your business into a rigid ERP mold, build custom modules that fit your business like a glove.

The Architecture

Here's the modular architecture I use for custom ERP modules in Go:

// Module interface that every ERP module must implement
type ERPModule interface {
    Name() string
    Version() string
    Init(db *sqlx.DB, aiClient *AIClient) error
    RegisterRoutes(api fiber.Router)
    RegisterHooks(hooks *HookRegistry)
    HealthCheck(ctx context.Context) error
}

// Base module with common functionality
type BaseModule struct {
    DB      *sqlx.DB
    AI      *AIClient
    Logger  *zerolog.Logger
    Config  *ModuleConfig
}

func (b *BaseModule) HealthCheck(ctx context.Context) error {
    return b.DB.PingContext(ctx)
}

Example: AI-Powered Purchase Module

Here's a complete purchase module with AI-powered features:

type PurchaseModule struct {
    BaseModule
}

func (m *PurchaseModule) Name() string { return "purchase" }
func (m *PurchaseModule) Version() string { return "1.0.0" }

func (m *PurchaseModule) RegisterRoutes(api fiber.Router) {
    purchase := api.Group("/purchase")
    
    purchase.Post("/orders", m.CreatePurchaseOrder)
    purchase.Get("/orders", m.ListPurchaseOrders)
    purchase.Post("/invoices/process", m.ProcessInvoice)
    purchase.Post("/suggest-vendor", m.SuggestVendor)
}

// AI-powered vendor suggestion
func (m *PurchaseModule) SuggestVendor(c *fiber.Ctx) error {
    var req VendorSuggestionRequest
    if err := c.BodyParser(&req); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "invalid request"})
    }
    
    // Get historical data
    history, err := m.getPurchaseHistory(c.Context(), req.ItemID)
    if err != nil {
        return err
    }
    
    // Build AI prompt with business context
    prompt := fmt.Sprintf(`
        Based on our purchase history, suggest the best vendor for:
        Item: %s
        Quantity: %d
        Required by: %s
        
        Purchase history:
        %s
        
        Consider: price, delivery time, quality rating, and reliability.
        Recommend the top 3 vendors with reasons.
    `, req.ItemName, req.Quantity, req.RequiredBy, formatHistory(history))
    
    suggestion, err := m.AI.Complete(c.Context(), prompt)
    if err != nil {
        return err
    }
    
    return c.JSON(fiber.Map{
        "suggestion": suggestion,
        "source":     "ai_analysis",
        "confidence": calculateConfidence(history),
    })
}

Database Schema Pattern

Every module gets its own schema prefix to avoid conflicts:

-- Purchase module tables
CREATE SCHEMA purchase;

CREATE TABLE purchase.orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_number VARCHAR(20) UNIQUE NOT NULL,
    vendor_id UUID NOT NULL REFERENCES core.vendors(id),
    status VARCHAR(20) DEFAULT 'draft',
    total_amount DECIMAL(15, 2) NOT NULL,
    tax_amount DECIMAL(15, 2) DEFAULT 0,
    notes TEXT,
    ai_suggestion JSONB,
    created_by UUID REFERENCES core.users(id),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE purchase.order_items (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_id UUID NOT NULL REFERENCES purchase.orders(id) ON DELETE CASCADE,
    product_id UUID NOT NULL REFERENCES core.products(id),
    quantity DECIMAL(10, 2) NOT NULL,
    unit_price DECIMAL(15, 2) NOT NULL,
    total_price DECIMAL(15, 2) GENERATED ALWAYS AS (quantity * unit_price) STORED
);

-- AI audit trail
CREATE TABLE purchase.ai_decisions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    entity_type VARCHAR(50) NOT NULL,
    entity_id UUID NOT NULL,
    decision_type VARCHAR(50) NOT NULL,
    prompt_used TEXT,
    ai_response TEXT,
    confidence DECIMAL(3, 2),
    human_override BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

Module Registration and Discovery

// main.go - Module registration
func main() {
    db := initDB()
    aiClient := initAI()
    
    // Register all modules
    modules := []ERPModule{
        &purchase.PurchaseModule{},
        &inventory.InventoryModule{},
        &finance.FinanceModule{},
        &hr.HRModule{},
    }
    
    app := fiber.New()
    api := app.Group("/api/v1")
    
    for _, mod := range modules {
        if err := mod.Init(db, aiClient); err != nil {
            log.Fatal().Err(err).Str("module", mod.Name()).Msg("module init failed")
        }
        mod.RegisterRoutes(api)
        log.Info().Str("module", mod.Name()).Str("version", mod.Version()).Msg("module loaded")
    }
    
    app.Listen(":8080")
}

Testing Modules

Each module is independently testable:

func TestPurchaseModule_CreateOrder(t *testing.T) {
    db := setupTestDB(t)
    aiClient := setupMockAI(t)
    
    mod := &PurchaseModule{}
    mod.Init(db, aiClient)
    
    app := fiber.New()
    mod.RegisterRoutes(app.Group("/api"))
    
    // Create order
    body := `{"vendor_id": "v1", "items": [{"product_id": "p1", "quantity": 10, "unit_price": 50000}]}`
    req := httptest.NewRequest("POST", "/api/purchase/orders", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    
    resp, err := app.Test(req)
    assert.NoError(t, err)
    assert.Equal(t, 201, resp.StatusCode)
    
    var result map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&result)
    assert.NotEmpty(t, result["id"])
}

Deployment

Each module is a separate Go package, compiled into a single binary:

# Build with all modules
go build -o erp-server ./cmd/server

# Or build with specific modules only
MODULES=purchase,inventory go build -o erp-server ./cmd/server

# Docker
docker build -t erp-server:latest .
docker run -p 8080:8080 erp-server:latest

Key Takeaways

The Bottom Line

Building custom ERP modules in Go with AI isn't rocket science. It's disciplined software engineering applied to business problems. Clean architecture, proper testing, and AI where it actually adds value — that's the formula.

← More articles from Cecep Azhar Central Content Hub Home →