This blog is the part of the series Building a Web App with Golang.
In today's article we are going to define the basic entities and the operations on them for our web application. In the first part we defined the scope of the application.
We are going to build a system that can issue issue invoices for clients.
Let's create a new git branch to work on:
git checkout -b basic-domain
Company
Every client and "us" (our company/the freelancer) will be represented by an entity Company
.
Let's create a file named company.go
in the root of our project
then we define a struct with the basic attributes we have:
package invoicehub
type Company struct {
ID int
Name string
Address Address
Email string
TaxID string
VatID string
BankAccounts []BankAccount
}
type Address struct {
Street string
City string
PostalCode string
Country string
}
type BankAccount struct {
BankName string
AccountNumber string
IBAN string
BIC string
}
The idea is that we will represent each real life company using the above struct.
Invoice
Create an invoice.go
in the root of the project
Each invoice will have at least the following:
- Seller
- Buyer
- Amount
- Issue Date
- Invoice Number
- Days (number of days after issuance that must be paid).
We need to install first one dependency to handle decimal numbers:
get github.com/shopspring/decimal
Now let's start with that:
package invoicehub
import (
"time"
"github.com/shopspring/decimal"
)
type Invoice struct {
ID int
InvoiceNumber string
IssueDate time.Time
SellerID int
BuyerID int
DaysToPay int
LineItems []LineItem
}
type LineItem struct {
Description string
Amount Amount
VatRate decimal.Decimal
}
type Amount struct {
Value decimal.Decimal
Currency string
}
The above structs should satisfy our needs for now.
Pay, attention that I am using ids for Buyer and Seller. We could have used a Company struct there.
Why I choose to use ids instead of a company struct?
Our application now is a "monolithic" application. In the future we might want to split into different microservices (maybe one microservice to handle the companies and one microservice to handle the invoices). Let's be proactive and try to decouple the Companies with the Invoices.
I does not look that we need other structs to represent our business domain. Of-course , we might forgotten something but then we will change.
Operations
Our application is pretty simple:
For a Company we need to be able to:
Create a company
Read a company
Update a company
Our requirements don't have a Delete so we skip for now.
Creating, Reading, Updating a company are straightforward.
First we need a way to do these operations in our storage layer.
For this we are going to create an interface CompanyRepository
Let's do that:
in your company.go
add the following:
type CompanyRepository interface {
Create(ctx context.Context, company *Company) (int, error)
Update(ctx context.Context, company *Company) error
Get(ctx context.Context, id int) (Company, error)
}
Now for our invoices:
We need to be able to :
Create Invoice
Read Invoice
Create a PDF from an invoice
we don't have full CRUD (Create Read Update Delete) since we don't want to Update or Delete invoices.
Let's define the repository also for invoices:
in the invoice.go
add:
type InvoiceRepository interface {
Create(ctx context.Context, invoice *Invoice) (int, error)
Get(ctx context.Context, id int) (Invoice, error)
}
What about PDF generation here? This is not a database operation.
Additionally, what about creation of an invoice? It is a little bit more complex.
In the requirements we had that the InvoiceNumber
looks like:
1031/24
so we have two parts:
1. a number that resets every year (1031)
2. the year (24)
So the next invoice should look like 1032/24 but if it is the first for 2025 should look like 1001/24 .
Notice that we start always with 1000.
This is just one way others, might do it differently.
It would be nice to have a generic way to compute that per customer. BUT, do not forget our requirements. We want to code for now and not for imaginary use cases. Let's get stuff done
Since invoices are a bit more complex let's also create a Service Layer that will handle the business logic .
open invoices.go
and add:
type InvoiceService interface {
Create(ctx context.Context, invoice *Invoice) error
Get(ctx context.Context, id int) (Invoice, error)
CreatePDF(ctx context.Context, id int) ([]byte, error)
}
We also need an extra method in the Repository that returns the last invoice of the year. We are going to use that to compute the invoice number
Add the GetLastInvoiceForYear
in the repository
Adding the interfaces in our HTTP layer
Now that we have "described" the operations of our system via the interfaces let's see where we are going to use them.
in the http/router.go
we defined a base handler :
Let's add the interfaces there:
type baseHandler struct {
e *echo.Echo
companies invoicehub.CompanyRepository
invoices invoicehub.InvoiceService
}
so when we create the baseHandler we should inject the implementations of the interfaces:
Since we don't have any concrete implementation yet let's pass nil where we call the NewRouter function in our cmd/main.go
.
Commit and Push
let's commit our code and push our branch
git add .
git commit -m "add basic domain entities"
git push origin basic-domain
And as usual, you can find today's code in the github branch
Summary and what is next
In today's blog post we started implementing our business logic. We first defined the required structs and the operations that we can do on these.
The operations are defined using interfaces. Utilizing interfaces allows us to think about our business logic without having to think about implementation details. Additionally, it will allow us to easily unit test our application later.
I would like to share a piece of advice here. When you have to develop an application no matter how complex it is try first to focus on what is your goal. Don't fall into the trap of questioning your self all the time like:
what if the user wants also that?
what if this changes ?
what if ... ?
These what if may never happen. If they do be confident that when it's required you will handle it.
I am not saying or implying do not think about scalability, changes of requirements etc . I am saying think about them try to code in a way that some decisions are deferred but don't waste your time trying to code something smart when it is not required.
The moments I am writing these lines I am thinking the following:
- How am I distinguish which company is the Seller when we don't have a User entity ?
- What if when we create an invoice someone else does it parallel (so the invoice number may come wrong?_
- How am i going to create an invoice number when I do it near the end of the year and I use UTC in the system but my current timezone is UTC+3 and I am in the next year?
- ...
and many many more.
Some of these are maybe valid concerns. But so far we don't have anything working yet. We don't have to aim for perfection, a working software is better that no software in the end.
Links to the other part of that series
https://blog.gkomninos.com/crafting-a-web-application-with-golang-a-step-by-step-guide
https://blog.gkomninos.com/setting-up-a-docker-development-enviroment-for-go
https://blog.gkomninos.com/building-a-robust-web-server-in-go-a-step-by-step-guide
https://blog.gkomninos.com/tutorial-deployment-of-golang-web-app-using-systemd
👍 Please leave a comment or like the article if you like it. This will help me gain visibility.
❤️ Following me on Github or X or LinkedIn motivates me keep writing.