# Factories & Seeding

> Fake users and tasks, seeded with one command.

Goravel series, part 7 of 20. By Krishan Kumar. Published 2026-10-23.
Source: https://devchalk.com/goravel/07-factories-seeding
Code: https://github.com/krishankumar01/goravel-taskflow/tree/part-07

So far every row came from `psql`, typed by hand. That was fine for a few rows, but it is slow, and you type them all again each time the tables are rebuilt. A factory describes one fake row. A seeder uses factories to fill the database. By the end of this part, one command rebuilds the database and fills it with users and tasks.

Start from the Relationships: Has Many code:

```bash
git clone https://github.com/krishankumar01/goravel-taskflow
cd goravel-taskflow
git checkout part-06
cp .env.example .env
go run . artisan key:generate
docker compose up -d
go run . artisan migrate
```

## The user factory

Create a factory for users:

```bash
$ go run . artisan make:factory UserFactory
 SUCCESS  Factory created successfully
```

It creates `database/factories/user_factory.go` with an empty `Definition`. Fill it in, using [gofakeit](https://github.com/brianvoe/gofakeit) for the fake values:

```go title="database/factories/user_factory.go"
package factories

import (
	"github.com/brianvoe/gofakeit/v7"

	"taskflow/app/facades"
)

type UserFactory struct {
}

// Definition Define the model's default state.
func (f *UserFactory) Definition() map[string]any {
	password, _ := facades.Hash().Make("password")

	return map[string]any{
		"Name":     gofakeit.Name(),
		"Email":    gofakeit.Email(),
		"Password": password,
		"Role":     "member",
	}
}
```

- The keys are the model's field names, like `DueAt`, not the column names, like `due_at`.
- Every fake user gets the password `password`, hashed with the `Hash` facade.
- The role is the plain string `"member"`, not `models.RoleMember`. The model will import this package, and Go does not let two packages import each other.

Then run:

```bash
$ go mod tidy
```

The model finds its factory through a `Factory` method:

```diff lang="go" title="app/models/user.go" collapse={10-16}
 package models
 
 import (
+	"github.com/goravel/framework/contracts/database/factory"
 	"github.com/goravel/framework/database/orm"
+
+	"taskflow/database/factories"
 )
 
 type Role string
 
 const (
 	RoleManager Role = "manager"
 	RoleMember  Role = "member"
 )
 
 type User struct {
 	orm.Model
 	Name     string `gorm:"size:255;not null" json:"name"`
 	Email    string `gorm:"size:255;unique;not null" json:"email"`
 	Password string `gorm:"size:255;not null" json:"-"`
 	Role     Role   `gorm:"type:enum('manager','member');default:'member';not null" json:"role"`
 
 	CreatedTasks  []Task `gorm:"foreignKey:CreatedBy" json:"created_tasks"`
 	AssignedTasks []Task `gorm:"foreignKey:AssignedTo" json:"assigned_tasks"`
 }
+
+func (u *User) Factory() factory.Factory {
+	return &factories.UserFactory{}
+}
```

## The task factory

```bash
$ go run . artisan make:factory TaskFactory
 SUCCESS  Factory created successfully
```

Fill in its `Definition`:

```go title="database/factories/task_factory.go"
package factories

import (
	"github.com/brianvoe/gofakeit/v7"
	"github.com/goravel/framework/support/carbon"
)

type TaskFactory struct {
}

// Definition Define the model's default state.
func (f *TaskFactory) Definition() map[string]any {
	title := gofakeit.RandomString([]string{
		"Fix the login bug",
		"Write the API docs",
		"Review the pull request",
		"Plan the next release",
		"Update the dependencies",
		"Add tests for tasks",
		"Clean up old logs",
		"Reply to support emails",
	})
	status := gofakeit.RandomString([]string{"pending", "in-progress", "done"})
	due := carbon.Now().AddDays(gofakeit.Number(1, 30)).StartOfDay().SetHour(9)

	return map[string]any{
		"Title":       title,
		"Description": gofakeit.HackerPhrase(),
		"Status":      status,
		"DueAt":       carbon.NewDateTime(due),
	}
}
```

Due dates are 1 to 30 days from today, at 9:00. `Status` is a plain string too, for the same reason as the role.

There is no `CreatedBy` or `AssignedTo`. Both need real users, so the seeder sets them.

Link the factory to the `Task` model:

```diff lang="go" title="app/models/task.go" collapse={11-17}
 package models
 
 import (
+	"github.com/goravel/framework/contracts/database/factory"
 	"github.com/goravel/framework/database/orm"
 	"github.com/goravel/framework/support/carbon"
+
+	"taskflow/database/factories"
 )
 
 type Status string
 
 const (
 	StatusPending    Status = "pending"
 	StatusInProgress Status = "in-progress"
 	StatusDone       Status = "done"
 )
 
 type Task struct {
 	orm.Model
 	CreatedBy   uint             `gorm:"not null" json:"created_by"`
 	Creator     *User            `gorm:"foreignKey:CreatedBy" json:"creator"`
 	AssignedTo  *uint            `json:"assigned_to"`
 	Assignee    *User            `gorm:"foreignKey:AssignedTo" json:"assignee"`
 	Title       string           `gorm:"size:255;not null" json:"title"`
 	Description string           `gorm:"type:text;not null" json:"description"`
 	Status      Status           `gorm:"type:enum('pending','in-progress','done');default:'pending';not null" json:"status"`
 	DueAt       *carbon.DateTime `json:"due_at"`
 }
+
+func (t *Task) Factory() factory.Factory {
+	return &factories.TaskFactory{}
+}
```

> **Code** The code for every step of this part is in the tag [`part-07`](https://github.com/krishankumar01/goravel-taskflow/tree/part-07).

## The user and task seeders

Make two seeders, one for users and one for tasks. Keeping them apart lets you run one alone, for example to add more tasks without touching the users:

```bash
$ go run . artisan make:seeder UserSeeder
 SUCCESS  Seeder created successfully
 SUCCESS  Seeder registered successfully
$ go run . artisan make:seeder TaskSeeder
 SUCCESS  Seeder created successfully
 SUCCESS  Seeder registered successfully
```

Like `make:migration`, `make:seeder` registers what it creates. It lists the seeders in a new `bootstrap/seeders.go`:

```go title="bootstrap/seeders.go"
package bootstrap

import (
	"github.com/goravel/framework/contracts/database/seeder"

	"taskflow/database/seeders"
)

func Seeders() []seeder.Seeder {
	return []seeder.Seeder{
		&seeders.UserSeeder{},
		&seeders.TaskSeeder{},
	}
}
```

`db:seed` runs them in this order. You made `UserSeeder` first, so the users exist before the tasks. `make:seeder` also added `WithSeeders(Seeders)` to `bootstrap/app.go`:

```diff lang="go" title="bootstrap/app.go" collapse={3-13} collapse={18-28}
 package bootstrap
 
 import (
 	"github.com/goravel/framework/contracts/database/schema"
 	contractsfoundation "github.com/goravel/framework/contracts/foundation"
 	"github.com/goravel/framework/foundation"
 
 	"taskflow/app/facades"
 	"taskflow/app/models"
 	"taskflow/config"
 	"taskflow/routes"
 )
 
 func Boot() contractsfoundation.Application {
 	return foundation.Setup().
+		WithSeeders(Seeders).
 		WithMigrations(Migrations).
 		WithRouting(func() {
 			routes.Api()
 			routes.Web()
 		}).
 		WithProviders(Providers).
 		WithConfig(config.Boot).
 		WithCallback(func() {
 			facades.Schema().Extend(schema.Extension{
 				Models: []any{models.User{}, models.Task{}},
 			})
 		}).
 		Create()
 }
```

Fill in `Run` in `database/seeders/user_seeder.go`:

```go title="database/seeders/user_seeder.go"
package seeders

import (
	"taskflow/app/facades"
	"taskflow/app/models"
)

type UserSeeder struct {
}

// Signature The name and signature of the seeder.
func (s *UserSeeder) Signature() string {
	return "UserSeeder"
}

// Run executes the seeder logic.
func (s *UserSeeder) Run() error {
	var manager models.User
	err := facades.Orm().Factory().Create(&manager, map[string]any{
		"Name":  "Ana",
		"Email": "ana@example.com",
		"Role":  models.RoleManager,
	})
	if err != nil {
		return err
	}

	var members []models.User
	return facades.Orm().Factory().Count(3).Create(&members)
}
```

The first `Create` makes Ana, the manager. The map overrides what the factory would pick, so Ana always has the email `ana@example.com`. Her password is `password`, like every fake user's. `Count(3)` then makes three members with fake names.

`TaskSeeder` can run alone, so it reads the users back from the database instead of getting them from `UserSeeder`. Fill in `database/seeders/task_seeder.go`:

```go title="database/seeders/task_seeder.go"
package seeders

import (
	"taskflow/app/facades"
	"taskflow/app/models"
)

type TaskSeeder struct {
}

// Signature The name and signature of the seeder.
func (s *TaskSeeder) Signature() string {
	return "TaskSeeder"
}

// Run executes the seeder logic.
func (s *TaskSeeder) Run() error {
	var manager models.User
	err := facades.Orm().Query().Where("role", models.RoleManager).FirstOrFail(&manager)
	if err != nil {
		return err
	}

	var members []models.User
	err = facades.Orm().Query().Where("role", models.RoleMember).Find(&members)
	if err != nil {
		return err
	}

	for _, member := range members {
		var tasks []models.Task
		err = facades.Orm().Factory().Count(2).Create(&tasks, map[string]any{
			"CreatedBy":  manager.ID,
			"AssignedTo": &member.ID,
		})
		if err != nil {
			return err
		}
	}

	var unassigned models.Task
	return facades.Orm().Factory().Create(&unassigned, map[string]any{
		"CreatedBy": manager.ID,
	})
}
```

- If there is no manager yet, `FirstOrFail` stops the seeder with `record not found`. `First` would hand back an empty user instead.
- `AssignedTo` takes `&member.ID` because the field is a pointer.
- One more task is made by Ana and assigned to nobody.

## Running seeders with db:seed

`db:seed` runs every registered seeder:

```bash
$ go run . artisan db:seed
  ERROR   fail to run seeder: ERROR: duplicate key value violates unique constraint "users_email_unique" (SQLSTATE 23505)
```

It fails because Ana is still in the table from the earlier parts, and emails must be unique. A seeder only adds rows. It does not clear the tables first. If you started from the code at the top of this part, your tables were empty and it worked.

## Resetting the database with migrate:fresh

`migrate:fresh` drops every table and runs all the migrations again, and `--seed` then runs the seeders. Run it either way, so your ids match the ones below:

```bash
$ go run . artisan migrate:fresh --seed
 SUCCESS  Dropped all tables successfully
  INFO    Running migration
  INFO    Running: 20210101000002_create_jobs_table
  INFO    Running: 20261022221906_create_users_table
  INFO    Running: 20261023063944_create_tasks_table
 SUCCESS  Migration success
 SUCCESS  Database seeding completed successfully.
 SUCCESS  Migration fresh success
```

The tables now hold the seeded rows:

```bash
$ docker compose exec postgres psql -U goravel -d taskflow \
    -c "SELECT id, name, email, role, left(password, 7) AS hash FROM users" \
    -c "SELECT id, assigned_to, title, status, due_at::date AS due FROM tasks"
 id |      name      |           email            |  role   |  hash   
----+----------------+----------------------------+---------+---------
  1 | Ana            | ana@example.com            | manager | $2a$12$
  2 | Darion Whitney | francis.hansen@jensen.name | member  | $2a$12$
  3 | Joany Silva    | cpowell@richards.org       | member  | $2a$12$
  4 | Cleora Grady   | ruby.murphy3@harris.io     | member  | $2a$12$
(4 rows)

 id | assigned_to |          title          |   status    |    due     
----+-------------+-------------------------+-------------+------------
  1 |           2 | Add tests for tasks     | in-progress | 2026-11-06
  2 |           2 | Write the API docs      | done        | 2026-11-05
  3 |           3 | Update the dependencies | done        | 2026-10-25
  4 |           3 | Fix the login bug       | pending     | 2026-11-15
  5 |           4 | Update the dependencies | in-progress | 2026-10-28
  6 |           4 | Write the API docs      | in-progress | 2026-11-02
  7 |             | Reply to support emails | in-progress | 2026-11-15
(7 rows)
```

Your names, emails and dates are different, because they are random. The passwords start with `$2a$12$`, the start of a bcrypt hash, so they are real hashes now, not `x`.

## Running one seeder

`--seeder` runs only the seeders you name, by the name their `Signature` returns. To get more tasks for the same users, run `TaskSeeder` alone:

```bash
$ go run . artisan db:seed --seeder=TaskSeeder
 SUCCESS  Database seeding completed successfully.
$ docker compose exec postgres psql -U goravel -d taskflow -c "SELECT count(*) FROM tasks"
 count 
-------
    14
(1 row)
```

The same users got seven more tasks. Running `UserSeeder` alone would fail on Ana's email, just like before. Run `migrate:fresh --seed` again whenever you want the starting data back.

---

Previous: [06. Relationships: Has Many](https://devchalk.com/goravel/06-relationships-has-many.md)
Series: [Goravel](https://devchalk.com/goravel.md)
