Skip to content

Blog

4 Steps to properly start a software project

As Software Engineers, we are all dealing with legacy projects. Sometimes we face a big ball of mud, sometimes we have to deal with an outdated framework spread everywhere. These situations are usually resulting from mistakes made early in a project’s life. It is important to avoid the common ones not to create a new piece of technical debt. I will give you four simple steps that will help you avoid these common pitfalls when starting a project and ensure its proper growth.

Step 1 : Create a version control repository

Using a version control system is essential. Here is my advice:

  • create a repository before you do anything else, so you will have a complete history of the project development (and not a big initial commit).
  • write concise and meaningful commit messages (some guidelines here), so your history is kept clean. Taking the time to explain the purpose of your changes in the commit message also makes you write better code.

Plenty of free solutions are out there, like GitHub or Gitlab, so setting up for a repository should not take long.

Time invested: a few minutes to start a project, a few minutes for each commit messages

Step 2 : Set up a CI pipeline

Before writing the code, set up a continuous integration pipeline. It should be really simple at first and then grow more complex alongside code. A good initial pipeline could look like this:

  • code check/compilation
  • tests
  • packaging
  • deployment

Having a CI/CD pipeline set up from the beginning brings many advantages with little investment:

  • automating and self-documenting maintenance of the project, reducing development time and human errors
  • enforcing policies (tests, coding guidelines and conventions, …), making your project more readable and less prone to bugs

Again, free tools are available (Circle CI, Travis CI, Gitlab CI, …) and easy to set up.

Time invested: a few hours

Step 3 : Write your business rules

This is the most important, but usually unconsidered step. People tend to start with choosing a framework, libraries, a database, a web server, etc. Those are hard decisions to get right at this stage, and you will probably end up making some wrong choices that will be difficult to fix later. You should delay these decisions as far as possible (see Step 4).

Instead, start with writing your business rules.

Business rules come from the business specifications of your MVP. They could be described as:

  • the entities that are the core of your business (they exist outside automation)
  • the use cases, that are automated operations playing with the entities

This terminology comes from Uncle Bob’s Clean Architecture book.

You should be able to write all these rules without any code or software dependencies. If you use a tool like Maven to compile and build your component, it simply means that dependencies section is empty. Try to be as pure as possible, using the language and the basic SDK only. By doing so, you will probably end up having some of the following abstractions:

  • you need persistence?
    Create an abstract interface for your persistence with adequate methods
  • you need to do a service call?
    Create an abstract interface and few data structures representing the service call
  • you need to offer a service over the wire?
    Create an abstract interface, data structures and an implementation representing this service

For testing the code, just implement these interfaces with simple and dummy in-memory objects.

This approach will result in multiple benefits:

  • it is easy to test, you don’t need an advanced mock library or set up complex test environments
  • it will help you identify business details and corner cases and make you ask questions to stakeholders
  • it will be easier to architecture the code

Time invested : days to a few weeks

Step 4 : Implement the abstraction

Before you can actually deploy functional software, you need to implement your abstractions. Having the interfaces used by your business rules should make the job easier. Maybe you don’t need a relational database after all. Maybe you can start running your service on a small HTTP server. Even if you realize you made the wrong decisions, having such boundaries will make them easier to fix.

You should also ensure that the implementations are not polluting the business rules. It is usually better to enforce that by splitting your code in different compilation units (for example, with Maven, you could have a project split in two modules, one without dependencies for business rules, the other for abstraction implementations).

Time invested: a few days

Final words

Using these steps should start you out on the right path and you can reuse them for future evolutions. Think always first in term of business rules, code them and then take care of abstraction implementations. It will make your code and your projects clean, maintainable and easier to evolve.

Java annotations and OOP

Annotations has been introduced in JDK 1.5 in order to put metadata along with the code. These metadata are available at compile time, or at runtime through reflection.

The purpose of this article is to see different use cases of annotations and how they fit regarding OOP principles.

The core ones

The package java.lang provides a set of basic annotations

  • Deprecated
  • FunctionalInterface
  • Override
  • SafeVarargs
  • SuppressWarnings

They have all the same goal : give additional information about the code to programmers and eventually run compilation checks. They have no effect nor implication with OOP. A class or method with or without these annotations will have the same run-time behavior.

Dependency inversion inverted

Dependency injection tools usually use annotations.

The Java EE ones :

  • Inject
  • Singleton
  • Resource
  • Named

Or custom ones :

  • Autowired

These annotations clearly break principles/good practices with OOP :

  • Coupling : the code of an object becomes coupled to the dependency injection annotations
  • Encapsulation : when injection is applied to the private state of the object, it is exposing this inner state to an external components

Here a small example of dependency inversion in plain Java

public interface DataLayer {
    Object readData(int id);
}

public class SqlLayer implements DataLayer {
    @Override
    public Object readData(int id) {
        // Do the job
    }
}

public class Service {
    private final DataLayer dataLayer;

    public Service(DataLayer dataLayer) {
        this.dataLayer = dataLayer;
    }
}


// Use a SqlLayer as singleton
DataLayer dataLayer = new SqlLayer();
Service serviceA = new Service(dataLayer);
Service serviceB = new Service(dataLayer);

// ... or use multiple one
Service serviceA = new Service(new SqlLayer());
Service serviceB = new Service(new SqlLayer());

By doing manually the injection, we can see that the code is totally decoupled. The Service or SqlLayer classes are totally decoupled to the way there are instantiated and injected.

Now a similar code with javax.inject annotations

public interface DataLayer {
    Object readData(int id);
}

@Singleton
public class SqlLayer implements DataLayer {
    @Override
    public Object readData(int id) {
        // Do the job
    }
}


public class Service {
    @Inject
    private DataLayer dataLayer;
}

Service serviceA = injector.getInstance(...);
Service serviceB = injector.getInstance(...);

With this annotation the decision of being a singleton is delegated to the SqlLayer object. But then why the SqlLayer class need to decide how it will be instantiated ? What if we want two different SqlLayer objects for integration tests ?

For the Service class, it is even worse as the "private" field will get accessed by an external system. We can still use the annotation on a constructor to avoid this issue, but then, why we have to annotate the constructor with Inject at all ? The DI tool should be able to find the constructor without having to change the original object code.

Swiss army knife data structure

One of the main motivation for introducing the annotations in Java in the first place was to provide metadata for library like :

  • Xml/Json serializations
  • ORM/Entity mapping with data storage
  • Bean code generation

All these usage apply not to objects but to data structures, and try to reduce the amount of code required to perform a task on these data structures.

Using data structures is a useful programming technique (usually come along "procedural programming") that makes easier to add new functionalities on top of a fixed structure, while OOP makes easier to add new classes without changing the functionalities.

The purpose of annotations here is to configure the functionalities running on top of the structure. It doesn't break this principle as we are not modifying the structure itself. The only concern is a little bit of code pollution and compilation dependencies :

  • you cannot compile the data structure without the functionality library. So for example, I will have to embed a Json library in my jar, even if I never use the serialization functionality in my code
  • you could end up having business logic data structure having package dependencies to both data layer implementation and view serialization implementation

So, some extra care need to be taken here. Depending on the scope of a data structure, it could be better to externalize a functionality (like writing "Serializer/Deserializer" classes)

Behavioral annotations

In specification like JAX-RS or in framework like Spring, annotations are also used to attach a behavior to an object. There are plenty of design patterns in classical OOP to end up with the same result. Mostly, the annotations are used as an alternative syntax, to try to be more concise, but usually with the cost of less maintainability and a mixing responsibility/concern.

Here an example of annotation way to define REST API

@Path("/api")
public class UserApi {

    @GET
    @Path("/user")
    @Produces(MediaType.APPLICATION_JSON)
    public User getUser() {
        // ...
    }

    @POST
    @Path("/user")
    @Consumes(MediaType.APPLICATION_JSON)
    public void createUser(User track) {
        // ...
    }
}

And here without annotations

public class UserApiImpl implements UserApi {
    public User getUser() {
        // ...
    }


    public void createUser(User track) {
        // ...
    }
}

// Initialization code
UserApi api = ...;
...
// Http routing code
router.get("/api/user").produces("application/json").handler(api::getUser);
router.post("/api/user").consumes("application/json").handler(api::createUser);

The second solution also clearly split the responsibility of routing from the business logic. It allows you to test on the HTTP routing with a mocked api implementation or binding multiple routes or even protocols to the same logic method.