├── .gitignore ├── LICENSE ├── README.md ├── domain1 ├── endpoint.go ├── endpoint │ ├── endpoint.go │ └── endpoint_test.go ├── entity.go ├── error.go ├── handler │ └── handler.go ├── repository.go ├── service.go ├── service │ ├── service.go │ └── service_test.go └── transport.go ├── domain2 ├── entity.go └── repository.go ├── domain3 └── entity.go ├── domain4 ├── domain1.go ├── domain1_test.go ├── domain2.go └── model.go ├── internal └── httptransport │ └── json.go ├── main.go ├── mock ├── context.go ├── domain1repo.go └── domain1service.go └── template └── domain1 └── index.tmpl /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Thanatat Tamtan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-ddd-service-boilerplate 2 | 3 | Go DDD Project Boilerplate 4 | -------------------------------------------------------------------------------- /domain1/endpoint.go: -------------------------------------------------------------------------------- 1 | package domain1 2 | 3 | import "context" 4 | 5 | // Endpoint is the domain1 endpoint 6 | type Endpoint interface { 7 | Create(context.Context, *CreateRequest) (*CreateResponse, error) 8 | } 9 | 10 | // Create 11 | type ( 12 | // CreateRequest is the request for create endpoint 13 | CreateRequest struct { 14 | Field1 string `json:"field1"` 15 | } 16 | 17 | // CreateResponse is the response for create endpoint 18 | CreateResponse struct { 19 | ID string `json:"id"` 20 | } 21 | ) 22 | -------------------------------------------------------------------------------- /domain1/endpoint/endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/acoshift/go-ddd-service-boilerplate/domain1" 7 | ) 8 | 9 | // New creates new domain1 endpoint 10 | func New(s domain1.Service) domain1.Endpoint { 11 | return &endpoint{s} 12 | } 13 | 14 | type endpoint struct { 15 | s domain1.Service 16 | } 17 | 18 | func (ep *endpoint) Create(ctx context.Context, req *domain1.CreateRequest) (*domain1.CreateResponse, error) { 19 | id, err := ep.s.Create(ctx, &domain1.Entity1{ 20 | Field2: domain1.Entity2{ 21 | Field1: req.Field1, 22 | }, 23 | }) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return &domain1.CreateResponse{ID: id}, nil 28 | } 29 | -------------------------------------------------------------------------------- /domain1/endpoint/endpoint_test.go: -------------------------------------------------------------------------------- 1 | package endpoint_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/acoshift/go-ddd-service-boilerplate/domain1" 10 | "github.com/acoshift/go-ddd-service-boilerplate/domain1/endpoint" 11 | "github.com/acoshift/go-ddd-service-boilerplate/mock" 12 | ) 13 | 14 | func TestCreate(t *testing.T) { 15 | ctx := mock.Context{} 16 | s := mock.Domain1Service{ 17 | CreateFunc: func(ctx context.Context, entity *domain1.Entity1) (entityID string, err error) { 18 | assert.Equal(t, "field1_data", entity.Field2.Field1) 19 | return "abc", nil 20 | }, 21 | } 22 | 23 | ep := endpoint.New(&s) 24 | resp, err := ep.Create(&ctx, &domain1.CreateRequest{ 25 | Field1: "field1_data", 26 | }) 27 | assert.NoError(t, err) 28 | assert.NotNil(t, resp) 29 | assert.Equal(t, "abc", resp.ID) 30 | } 31 | -------------------------------------------------------------------------------- /domain1/entity.go: -------------------------------------------------------------------------------- 1 | package domain1 2 | 3 | import ( 4 | "github.com/acoshift/go-ddd-service-boilerplate/domain3" 5 | ) 6 | 7 | // Entity1 type 8 | type Entity1 struct { 9 | ID string 10 | Field1 string 11 | Field2 Entity2 12 | Field3 int 13 | Field4 domain3.Entity1 14 | } 15 | 16 | // Entity2 type 17 | type Entity2 struct { 18 | Field1 string 19 | Field2 bool 20 | } 21 | -------------------------------------------------------------------------------- /domain1/error.go: -------------------------------------------------------------------------------- 1 | package domain1 2 | 3 | import "errors" 4 | 5 | // domain1 errors 6 | var ( 7 | ErrEntity1NotFound = errors.New("domain1: entity1 not found") 8 | ) 9 | -------------------------------------------------------------------------------- /domain1/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | 7 | "github.com/acoshift/go-ddd-service-boilerplate/domain1" 8 | ) 9 | 10 | // New creates new domain1 handler 11 | func New(s domain1.Service) http.Handler { 12 | c := ctrl{} 13 | c.s = s 14 | 15 | c.templates = make(map[string]*template.Template) 16 | c.templates["index"] = template.Must(template.ParseFiles("template/domain1/index.tmpl")) 17 | 18 | mux := http.NewServeMux() 19 | 20 | mux.Handle("/", http.HandlerFunc(c.Index)) 21 | 22 | return mux 23 | } 24 | 25 | type ctrl struct { 26 | templates map[string]*template.Template 27 | s domain1.Service 28 | } 29 | 30 | func (c *ctrl) render(w http.ResponseWriter, name string, data interface{}) { 31 | tmpl := c.templates[name] 32 | if tmpl == nil { 33 | // this can panic, since it should never happened in production 34 | panic("template not found") 35 | } 36 | 37 | w.Header().Set("Content-Type", "text/html; chatset=utf-8") 38 | tmpl.Execute(w, data) 39 | } 40 | 41 | func (c *ctrl) Index(w http.ResponseWriter, r *http.Request) { 42 | c.render(w, "index", nil) 43 | } 44 | -------------------------------------------------------------------------------- /domain1/repository.go: -------------------------------------------------------------------------------- 1 | package domain1 2 | 3 | import "context" 4 | 5 | // Repository is the domain1 storage 6 | type Repository interface { 7 | // Registers inserts given Entity1 into storage 8 | Register(ctx context.Context, entity *Entity1) (entityID string, err error) 9 | 10 | // SetField3 sets field3 for Entity1 11 | SetField3(ctx context.Context, entityID string, field3 int) error 12 | } 13 | -------------------------------------------------------------------------------- /domain1/service.go: -------------------------------------------------------------------------------- 1 | package domain1 2 | 3 | import "context" 4 | 5 | // Service is the domain1 service 6 | type Service interface { 7 | // Create creates new Entity1 8 | Create(ctx context.Context, entity *Entity1) (entityID string, err error) 9 | 10 | // Update updates Entity1 11 | Update(ctx context.Context, entity *Entity1) error 12 | 13 | // Logic1 does logic1 14 | Logic1(ctx context.Context, arg1 string, arg2 int) error 15 | 16 | // Logic3 does logic2 17 | Logic2(ctx context.Context, entityID string) error 18 | } 19 | -------------------------------------------------------------------------------- /domain1/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/acoshift/go-ddd-service-boilerplate/domain1" 7 | ) 8 | 9 | // New creates new domain1 service 10 | func New(repo domain1.Repository) domain1.Service { 11 | return &service{repo} 12 | } 13 | 14 | type service struct { 15 | repo domain1.Repository 16 | } 17 | 18 | func (s *service) Create(ctx context.Context, entity *domain1.Entity1) (entityID string, err error) { 19 | return s.repo.Register(ctx, entity) 20 | } 21 | 22 | func (s *service) Update(ctx context.Context, entity *domain1.Entity1) error { 23 | return nil 24 | } 25 | 26 | func (s *service) Logic1(ctx context.Context, arg1 string, arg2 int) error { 27 | return nil 28 | } 29 | 30 | func (s *service) Logic2(ctx context.Context, entityID string) error { 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /domain1/service/service_test.go: -------------------------------------------------------------------------------- 1 | package service_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/acoshift/go-ddd-service-boilerplate/domain1" 10 | "github.com/acoshift/go-ddd-service-boilerplate/domain1/service" 11 | "github.com/acoshift/go-ddd-service-boilerplate/mock" 12 | ) 13 | 14 | func TestServiceCreate(t *testing.T) { 15 | ctx := mock.Context{} 16 | repo := mock.Domain1Repository{ 17 | RegisterFunc: func(ctx context.Context, entity *domain1.Entity1) (string, error) { 18 | assert.NotNil(t, entity) 19 | return "abc", nil 20 | }, 21 | } 22 | 23 | s := service.New(&repo) 24 | 25 | id, err := s.Create(&ctx, &domain1.Entity1{}) 26 | assert.NoError(t, err) 27 | assert.Equal(t, "abc", id) 28 | } 29 | -------------------------------------------------------------------------------- /domain1/transport.go: -------------------------------------------------------------------------------- 1 | package domain1 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/acoshift/go-ddd-service-boilerplate/internal/httptransport" 7 | ) 8 | 9 | type httpError struct { 10 | Message string `json:"message"` 11 | } 12 | 13 | // NewHTTPTransport creates new HTTP transport for domain1 14 | func NewHTTPTransport(ep Endpoint) http.Handler { 15 | mux := http.NewServeMux() 16 | 17 | errorEncoder := func(w http.ResponseWriter, err error) { 18 | status := http.StatusInternalServerError 19 | switch err { 20 | case ErrEntity1NotFound: 21 | status = http.StatusNotFound 22 | } 23 | httptransport.EncodeJSON(w, status, &httpError{Message: err.Error()}) 24 | } 25 | 26 | mux.Handle("/create", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | var req CreateRequest 28 | err := httptransport.DecodeJSON(r.Body, &req) 29 | if err != nil { 30 | errorEncoder(w, err) 31 | return 32 | } 33 | resp, err := ep.Create(r.Context(), &req) 34 | if err != nil { 35 | errorEncoder(w, err) 36 | return 37 | } 38 | httptransport.EncodeJSON(w, http.StatusOK, &resp) 39 | })) 40 | 41 | // or use https://github.com/acoshift/hrpc for RPC-HTTP style API 42 | // mux.Handle("/create", m.Handler(ep.Create)) 43 | 44 | return mux 45 | } 46 | -------------------------------------------------------------------------------- /domain2/entity.go: -------------------------------------------------------------------------------- 1 | package domain2 2 | 3 | import ( 4 | "github.com/acoshift/go-ddd-service-boilerplate/domain3" 5 | ) 6 | 7 | // Entity1 type 8 | type Entity1 struct { 9 | ID string 10 | Field1 string 11 | Field2 domain3.Entity1 12 | } 13 | -------------------------------------------------------------------------------- /domain2/repository.go: -------------------------------------------------------------------------------- 1 | package domain2 2 | 3 | import "context" 4 | 5 | // Repository is the domain2 storage 6 | type Repository interface { 7 | // Registers inserts given Entity1 into storage 8 | Register(ctx context.Context, entity *Entity1) (entityID string, err error) 9 | } 10 | -------------------------------------------------------------------------------- /domain3/entity.go: -------------------------------------------------------------------------------- 1 | package domain3 2 | 3 | // Entity1 type 4 | type Entity1 struct { 5 | Field1 string 6 | } 7 | -------------------------------------------------------------------------------- /domain4/domain1.go: -------------------------------------------------------------------------------- 1 | package domain4 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/acoshift/go-ddd-service-boilerplate/domain1" 7 | ) 8 | 9 | // NewDomain1Repository creates domain1 repository implements by domain4 10 | func NewDomain1Repository() domain1.Repository { 11 | return &domain1Repository{} 12 | } 13 | 14 | type domain1Repository struct{} 15 | 16 | func (domain1Repository) Register(ctx context.Context, entity *domain1.Entity1) (string, error) { 17 | return "", nil 18 | } 19 | 20 | // SetField3 sets field3 for Entity1 21 | func (domain1Repository) SetField3(ctx context.Context, entityID string, field3 int) error { 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /domain4/domain1_test.go: -------------------------------------------------------------------------------- 1 | package domain4_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/acoshift/go-ddd-service-boilerplate/domain1" 9 | "github.com/acoshift/go-ddd-service-boilerplate/domain4" 10 | "github.com/acoshift/go-ddd-service-boilerplate/mock" 11 | ) 12 | 13 | func TestDomain1RepositoryRegister(t *testing.T) { 14 | // integrate test with real database 15 | 16 | // init database 17 | // defer cleanup database 18 | 19 | repo := domain4.NewDomain1Repository() 20 | ctx := mock.Context{} 21 | // inject db to context 22 | id, err := repo.Register(&ctx, &domain1.Entity1{}) 23 | assert.NoError(t, err) 24 | assert.NotEmpty(t, id) 25 | } 26 | -------------------------------------------------------------------------------- /domain4/domain2.go: -------------------------------------------------------------------------------- 1 | package domain4 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/acoshift/go-ddd-service-boilerplate/domain2" 7 | ) 8 | 9 | // NewDomain2Repository creates new domain2 repository implements by domain4 10 | func NewDomain2Repository() domain2.Repository { 11 | return &domain2Repository{} 12 | } 13 | 14 | type domain2Repository struct{} 15 | 16 | func (domain2Repository) Register(ctx context.Context, entity *domain2.Entity1) (string, error) { 17 | return "", nil 18 | } 19 | -------------------------------------------------------------------------------- /domain4/model.go: -------------------------------------------------------------------------------- 1 | package domain4 2 | 3 | type domain1Table1Model struct { 4 | ID string 5 | Field1 string 6 | Field3 int 7 | } 8 | 9 | type domain1Table2Model struct { 10 | Field1 string 11 | } 12 | 13 | type domain2Table1Model struct { 14 | } 15 | 16 | type domain3Table1Model struct { 17 | } 18 | -------------------------------------------------------------------------------- /internal/httptransport/json.go: -------------------------------------------------------------------------------- 1 | package httptransport 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | const maxJSONBodySize = 10 << 20 // 10 MiB 10 | 11 | // DecodeJSON decodes json 12 | func DecodeJSON(r io.Reader, v interface{}) error { 13 | return json.NewDecoder(io.LimitReader(r, maxJSONBodySize)).Decode(v) 14 | } 15 | 16 | // EncodeJSON encodes json 17 | func EncodeJSON(w http.ResponseWriter, status int, v interface{}) error { 18 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 19 | w.WriteHeader(status) 20 | return json.NewEncoder(w).Encode(v) 21 | } 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/acoshift/go-ddd-service-boilerplate/domain1" 7 | domain1endpoint "github.com/acoshift/go-ddd-service-boilerplate/domain1/endpoint" 8 | domain1handler "github.com/acoshift/go-ddd-service-boilerplate/domain1/handler" 9 | domain1service "github.com/acoshift/go-ddd-service-boilerplate/domain1/service" 10 | "github.com/acoshift/go-ddd-service-boilerplate/domain4" 11 | ) 12 | 13 | func main() { 14 | // init repos 15 | domain1Repo := domain4.NewDomain1Repository() 16 | 17 | // init services 18 | domain1Service := domain1service.New(domain1Repo) 19 | 20 | // init endpoints 21 | domain1Endpoint := domain1endpoint.New(domain1Service) 22 | 23 | mux := http.NewServeMux() 24 | mux.Handle("/", domain1handler.New(domain1Service)) 25 | mux.Handle("/domain1/", http.StripPrefix("/domain1", domain1.NewHTTPTransport(domain1Endpoint))) 26 | 27 | http.ListenAndServe(":8080", mux) 28 | } 29 | -------------------------------------------------------------------------------- /mock/context.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import "context" 4 | 5 | // Context mocks context 6 | type Context struct { 7 | context.Context 8 | // mock context 9 | } 10 | -------------------------------------------------------------------------------- /mock/domain1repo.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/acoshift/go-ddd-service-boilerplate/domain1" 7 | ) 8 | 9 | // Domain1Repository is the mock struct for domain1 repository 10 | type Domain1Repository struct { 11 | RegisterFunc func(ctx context.Context, entity *domain1.Entity1) (string, error) 12 | SetField3Func func(ctx context.Context, entityID string, field3 int) error 13 | } 14 | 15 | // Register calls RegisterFunc 16 | func (r *Domain1Repository) Register(ctx context.Context, entity *domain1.Entity1) (string, error) { 17 | return r.RegisterFunc(ctx, entity) 18 | } 19 | 20 | // SetField3 calls SetField3 func 21 | func (r *Domain1Repository) SetField3(ctx context.Context, entityID string, field3 int) error { 22 | return r.SetField3Func(ctx, entityID, field3) 23 | } 24 | -------------------------------------------------------------------------------- /mock/domain1service.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/acoshift/go-ddd-service-boilerplate/domain1" 7 | ) 8 | 9 | // Domain1Service mocks domain1 service 10 | type Domain1Service struct { 11 | CreateFunc func(ctx context.Context, entity *domain1.Entity1) (entityID string, err error) 12 | UpdateFunc func(ctx context.Context, entity *domain1.Entity1) error 13 | Logic1Func func(ctx context.Context, arg1 string, arg2 int) error 14 | Logic2Func func(ctx context.Context, entityID string) error 15 | } 16 | 17 | // Create calls CreateFunc 18 | func (s *Domain1Service) Create(ctx context.Context, entity *domain1.Entity1) (entityID string, err error) { 19 | return s.CreateFunc(ctx, entity) 20 | } 21 | 22 | // Update calls UpdateFunc 23 | func (s *Domain1Service) Update(ctx context.Context, entity *domain1.Entity1) error { 24 | return s.UpdateFunc(ctx, entity) 25 | } 26 | 27 | // Logic1 calls Logic1Func 28 | func (s *Domain1Service) Logic1(ctx context.Context, arg1 string, arg2 int) error { 29 | return s.Logic1Func(ctx, arg1, arg2) 30 | } 31 | 32 | // Logic2 calls Logic2Func 33 | func (s *Domain1Service) Logic2(ctx context.Context, entityID string) error { 34 | return s.Logic2Func(ctx, entityID) 35 | } 36 | -------------------------------------------------------------------------------- /template/domain1/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |